Skip to content

Commit

Permalink
Merge pull request #143 from michielbdejong/1200156-custom-id-schemas
Browse files Browse the repository at this point in the history
Fixes #138 - Implement custom id schema's
  • Loading branch information
michielbdejong committed Sep 15, 2015
2 parents 8900071 + 82b00c2 commit 6ad02a6
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 73 deletions.
76 changes: 72 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
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 All @@ -85,7 +86,7 @@ Result:

> #### Notes
>
> - The promise will be rejected if no record is found for that id.
> - The promise will be rejected if no record is found for that ID.
## Updating a record

Expand Down Expand Up @@ -117,7 +118,7 @@ Result is:

> #### Notes
>
> - An id is required, otherwise the promise will be rejected.
> - An ID is required, otherwise the promise will be rejected.
## Deleting records

Expand All @@ -144,7 +145,7 @@ Result:

> #### Notes
>
> - An id is required, otherwise the promise will be rejected;
> - An ID is required, otherwise the promise will be rejected;
> - Virtual deletions aren't retrieved when calling `#get()` and `#list()`.
## Listing records
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
Original file line number Diff line number Diff line change
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
39 changes: 22 additions & 17 deletions src/collection.js
Original file line number Diff line number Diff line change
@@ -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,27 @@ 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} useRecordId 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={useRecordId: 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.useRecordId ? 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 +204,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 +222,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.idSchema.validate(id)) {
return Promise.reject(Error(`Invalid Id: ${id}`));
}
return this.db.get(id).then(record => {
if (!record ||
Expand All @@ -244,13 +249,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 6ad02a6

Please sign in to comment.