Skip to content

Commit

Permalink
Custom id schemas, fix #138
Browse files Browse the repository at this point in the history
  • Loading branch information
Michiel de Jong committed Sep 8, 2015
1 parent ddc5979 commit 8f880d4
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 75 deletions.
70 changes: 69 additions & 1 deletion docs/api.md
Expand Up @@ -60,7 +60,8 @@ Result is:

> #### Notes
>
> - Records identifiers are generated locally using [UUID v4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29).
> - By default, records identifiers are generated locally using [UUID v4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29),
but you can also define a [custom id schema](#id-schemas)).

## Retrieving a single record

Expand Down Expand Up @@ -558,3 +559,70 @@ coll = kinto.collection("articles", {
There's currently no way to deal with adding transformers to an already filled remote database; that would mean remote data migrations, and both Kinto and Kinto.js don't provide this feature just yet.

**As a rule of thumb, you should only start using transformers on an empty remote collection.**

## Id schemas

By default, kinto.js uses UUID4 strings for record id's. If you want to work with an existing body of data, this may not be what you want.

You can define a custom id schema on a collection by passing it to `Kinto#collection`:

```js
import Kinto from "kinto";

class IntegerIdSchema extends IdSchema {
constructor() {
super();
this._next = 0;
}
generate() {
return this._next++;
}
validate(id) {
return (typeof id == "number");
}
}

const kinto = new Kinto({remote: "https://my.server.tld/v1"});
coll = kinto.collection("articles", {
idSchema: new IntegerIdSchema()
});
```

> #### Notes
>
> - The `generate` method should generate unique id's;
> - The `validate` method should return a boolean, where `true` means valid.
> - In a real application, you want to make sure you do not generate twice the same record id on a collection. This dummy example doesn't take care of id unicity. In case of id conflict you may loose data.
### Creating id schemas in ES5

If your JavaScript environment doesn't suppport [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) just yet, you can derive id schemas in an ES5 fashion using `Kinto.createIdSchema` static helper:

```js
var IntegerIdSchema = Kinto.createIdSchema({
constructor() {
this._next = 0;
}
generate() {
return this._next++;
}
validate(id) {
return ((id == parseInt(id, 10)) && (id >= 0));
}
});

coll = kinto.collection("articles", {
idSchema: new IntegerIdSchema()
});
```

> #### Notes
>
> Notice you can define a `constructor` method, though you don't need (and must not) call `super()` within it, unlike with ES6 classes.
### Limitations

There's currently no way to deal with changing the id schema of an already filled local database; that would mean existing records would fail the new validation check, and can no longer be updated.

**As a rule of thumb, you should only start using a custom id schema on an empty remote collection.**
###
2 changes: 1 addition & 1 deletion src/adapters/IDB.js
Expand Up @@ -39,7 +39,7 @@ export default class IDB extends BaseAdapter {
const collStore = db.createObjectStore(this.dbname, {
keyPath: "id"
});
// Primary key (UUID)
// Primary key (generated by IdSchema, UUID by default)
collStore.createIndex("id", "id", { unique: true });
// Local record status ("synced", "created", "updated", "deleted")
collStore.createIndex("_status", "_status");
Expand Down
45 changes: 27 additions & 18 deletions src/collection.js
@@ -1,13 +1,14 @@
"use strict";

import { EventEmitter } from "events";
import { v4 as uuid4 } from "uuid";
import deepEquals from "deep-eql";

import BaseAdapter from "./adapters/base";
import { attachFakeIDBSymbolsTo, reduceRecords, isUUID4, waterfall } from "./utils";
import { attachFakeIDBSymbolsTo, reduceRecords, waterfall } from "./utils";
import { cleanRecord } from "./api";

import IdSchema from "./plugins/idschema";

import IDB from "./adapters/IDB";

attachFakeIDBSymbolsTo(typeof global === "object" ? global : window);
Expand Down Expand Up @@ -61,6 +62,7 @@ export default class Collection {
this._name = name;
this._lastModified = null;
this._plugins = {
idSchema: new IdSchema(),
remoteTransformers: []
};
const DBAdapter = options.adapter || IDB;
Expand Down Expand Up @@ -121,12 +123,17 @@ export default class Collection {
throw new Error("remoteTransformers should be an Array");
}
value.map(item => {
if (item.type !== "remote") {
if (item.type !== "remotetransformer") {
throw new Error("All remote transformers should inherit from " +
"Kinto.transformers.RemoteTransformer");
}
});
break;
case "idSchema":
if (typeof value !== "object" || value.type !== "idschema") {
throw new Error("Id schema should inherit from Kinto.IdSchema");
}
break;
default:
throw new Error(`Plugin type ${type} not supported`);
}
Expand Down Expand Up @@ -170,24 +177,26 @@ export default class Collection {
* Adds a record to the local database.
*
* Options:
* - {Boolean} synced Sets record status to "synced" (default: false).
* - {Boolean} forceUUID Enforces record creation using any provided UUID
* (default: false).
* - {Boolean} synced Sets record status to "synced" (default: false).
* - {Boolean} forceId Forces the id field from the record to be used, instead
* of one that is generated automatically (default: false).
*
* @param {Object} record
* @param {Object} options
* @return {Promise}
*/
create(record, options={forceUUID: false, synced: false}) {
create(record, options={forceId: false, synced: false}) {
if (typeof(record) !== "object") {
return Promise.reject(new Error("Record is not an object."));
}
const newRecord = Object.assign({}, record, {
id: options.synced || options.forceUUID ? record.id : uuid4(),
id: options.synced ||
options.forceId ? record.id :
this._plugins.idSchema.generate(),
_status: options.synced ? "synced" : "created"
});
if (!isUUID4(newRecord.id)) {
return Promise.reject(new Error(`Invalid UUID: ${newRecord.id}`));
if (!this._plugins.idSchema.validate(newRecord.id)) {
return Promise.reject(new Error(`Invalid Id: ${newRecord.id}`));
}
return this.db.create(newRecord).then(record => {
return {data: record, permissions: {}};
Expand All @@ -211,8 +220,8 @@ export default class Collection {
if (!record.id) {
return Promise.reject(new Error("Cannot update a record missing id."));
}
if (!isUUID4(record.id)) {
return Promise.reject(new Error(`Invalid UUID: ${record.id}`));
if (!this._plugins.idSchema.validate(record.id)) {
return Promise.reject(new Error(`Invalid Id: ${record.id}`));
}
return this.get(record.id).then(_ => {
var newStatus = "updated";
Expand Down Expand Up @@ -244,15 +253,15 @@ export default class Collection {
}

/**
* Retrieve a record by its uuid from the local database.
* Retrieve a record by its id from the local database.
*
* @param {String} id
* @param {Object} options
* @return {Promise}
*/
get(id, options={includeDeleted: false}) {
if (!isUUID4(id)) {
return Promise.reject(Error(`Invalid UUID: ${id}`));
if (!this._plugins.idSchema.validate(id)) {
return Promise.reject(Error(`Invalid Id: ${id}`));
}
return this.db.get(id).then(record => {
if (!record ||
Expand All @@ -271,13 +280,13 @@ export default class Collection {
* - {Boolean} virtual: When set to true, doesn't actually delete the record,
* update its _status attribute to "deleted" instead.
*
* @param {String} id The record's UUID.
* @param {String} id The record's Id.
* @param {Object} options The options object.
* @return {Promise}
*/
delete(id, options={virtual: true}) {
if (!isUUID4(id)) {
return Promise.reject(new Error(`Invalid UUID: ${id}`));
if (!this._plugins.idSchema.validate(id)) {
return Promise.reject(new Error(`Invalid Id: ${id}`));
}
// Ensure the record actually exists.
return this.get(id, {includeDeleted: true}).then(res => {
Expand Down
37 changes: 36 additions & 1 deletion src/index.js
Expand Up @@ -9,7 +9,8 @@ import Collection from "./collection";
import BaseAdapter from "./adapters/base";
import LocalStorage from "./adapters/LocalStorage";
import IDB from "./adapters/IDB";
import RemoteTransformer from "./transformers/remote";
import RemoteTransformer from "./plugins/remotetransformer";
import IdSchema from "./plugins/idschema";

const DEFAULT_BUCKET_NAME = "default";
const DEFAULT_REMOTE = "http://localhost:8888/v1";
Expand Down Expand Up @@ -44,6 +45,17 @@ export default class Kinto {
};
}

/**
* Provides a public access to base IdSchema class. Users can create
* custom id schemas by extending these.
*
* @return {Class}
*/
static get IdSchema() {
return IdSchema;
}


/**
* Creates a remote transformer constructor, the ES5 way.
*
Expand All @@ -67,6 +79,29 @@ export default class Kinto {
return _RemoteTransformer;
}

/**
* Creates an id schema constructor, the ES5 way.
*
* @return {IdSchema}
*/
static createIdSchema(proto) {
if (!proto || typeof proto !== "object") {
throw new Error("Expected prototype object.");
}

class _IdSchema extends IdSchema {
constructor() {
super();
// If a constructor is passed from the proto object, apply it.
if (proto.constructor) {
proto.constructor.apply(this, arguments);
}
}
}
_IdSchema.prototype = Object.assign(_IdSchema.prototype, proto);
return _IdSchema;
}

/**
* Constructor.
*
Expand Down
25 changes: 25 additions & 0 deletions src/plugins/idschema.js
@@ -0,0 +1,25 @@
"use strict";

import { v4 as uuid4 } from "uuid";
import { isUUID4 } from "../utils";

/**
* Id schema class, providing an interface for generating and validating
* id's.
*
* This class is provided as a base class you should extend to implement your
* own id schemas.
*/
export default class IdSchema {
get type() {
return "idschema";
}

generate() {
return uuid4();
}

validate(id) {
return isUUID4(id);
}
}
Expand Up @@ -9,7 +9,7 @@
*/
export default class RemoteTransformer {
get type() {
return "remote";
return "remotetransformer";
}

encode() {
Expand Down

0 comments on commit 8f880d4

Please sign in to comment.