Skip to content

Commit

Permalink
Fixes #138 - Implement custom id schema's
Browse files Browse the repository at this point in the history
  • Loading branch information
Michiel de Jong committed Sep 14, 2015
1 parent 8900071 commit 83cb854
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 70 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
53 changes: 36 additions & 17 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 UuidSchema from "./schemas/uuidschema";

import IDB from "./adapters/IDB";

attachFakeIDBSymbolsTo(typeof global === "object" ? global : window);
Expand Down Expand Up @@ -79,6 +80,7 @@ export default class Collection {
this.db = db;
this.api = api;
this.events = options.events || new EventEmitter();
this.idSchema = options.idSchema || new UuidSchema();
this.remoteTransformers = options.remoteTransformers || [];
}

Expand Down Expand Up @@ -158,24 +160,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.idSchema.generate(),
_status: options.synced ? "synced" : "created"
});
if (!isUUID4(newRecord.id)) {
return Promise.reject(new Error(`Invalid UUID: ${newRecord.id}`));
if (!this.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 @@ -199,8 +203,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.idSchema.validate(record.id)) {
return Promise.reject(new Error(`Invalid Id: ${record.id}`));
}
return this.get(record.id).then(_ => {
var newStatus = "updated";
Expand All @@ -217,15 +221,30 @@ export default class Collection {
}

/**
* Retrieve a record by its uuid from the local database.
* Resolves a conflict, updating local record according to proposed
* resolution — keeping remote record last_modified value as a reference for
* further batch sending.
*
* @param {Object} conflict The conflict object.
* @param {Object} resolution The proposed record.
* @return {Promise}
*/
resolve(conflict, resolution) {
return this.update(Object.assign({}, resolution, {
last_modified: conflict.remote.last_modified
}));
}

/**
* 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.idSchema.validate(id)) {
return Promise.reject(Error(`Invalid Id: ${id}`));
}
return this.db.get(id).then(record => {
if (!record ||
Expand All @@ -244,13 +263,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.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
36 changes: 36 additions & 0 deletions src/index.js
Expand Up @@ -9,6 +9,7 @@ import Collection from "./collection";
import BaseAdapter from "./adapters/base";
import LocalStorage from "./adapters/LocalStorage";
import IDB from "./adapters/IDB";
import IdSchema from "./schemas/idschema";
import RemoteTransformer from "./transformers/remote";

const DEFAULT_BUCKET_NAME = "default";
Expand Down Expand Up @@ -57,6 +58,17 @@ export default class Kinto {
return Collection.strategy;
}

/**
* 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 @@ -80,6 +92,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 Expand Up @@ -131,6 +166,7 @@ export default class Kinto {
events: this._options.events,
adapter: this._options.adapter,
dbPrefix: this._options.dbPrefix,
idSchema: options.idSchema,
remoteTransformers: options.remoteTransformers
});
}
Expand Down
22 changes: 22 additions & 0 deletions src/schemas/idschema.js
@@ -0,0 +1,22 @@
"use strict";

/**
* 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() {
throw new Error("Not implemented.");
}

validate(id) {
throw new Error("Not implemented.");
}
}
19 changes: 19 additions & 0 deletions src/schemas/uuidschema.js
@@ -0,0 +1,19 @@
"use strict";

import IdSchema from "./idschema";

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

/**
* The UUID4-based IdSchema used by default for Kinto collections.
*/
export default class UuidSchema extends IdSchema {
generate() {
return uuid4();
}

validate(id) {
return isUUID4(id);
}
}

0 comments on commit 83cb854

Please sign in to comment.