Skip to content

Commit

Permalink
Add support for custom id generation
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed May 3, 2024
1 parent d64fa54 commit 51de8e4
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 28 deletions.
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ Operators are specified as suffixes on each filtered field. For instance, applyi

```js
// initialize a rest server with a custom base URL
var restServer = new FakeRest.Server('http://my.custom.domain'); // // only URLs starting with my.custom.domain will be intercepted
const restServer = new FakeRest.Server('http://my.custom.domain'); // // only URLs starting with my.custom.domain will be intercepted
restServer.toggleLogging(); // logging is off by default, enable it to see network calls in the console
// Set all JSON data at once - only if identifier name is 'id'
restServer.init(json);
Expand Down Expand Up @@ -406,24 +406,45 @@ restServer.setDefaultQuery(function(resourceName) {
restServer.setBatchUrl('/batch');

// you can create more than one fake server to listen to several domains
var restServer2 = new FakeRest.Server('http://my.other.domain');
const restServer2 = new FakeRest.Server('http://my.other.domain');
// Set data collection by collection - allows to customize the identifier name
var authorsCollection = new FakeRest.Collection([], '_id');
const authorsCollection = new FakeRest.Collection([], '_id');
authorsCollection.addOne({ first_name: 'Leo', last_name: 'Tolstoi' }); // { _id: 0, first_name: 'Leo', last_name: 'Tolstoi' }
authorsCollection.addOne({ first_name: 'Jane', last_name: 'Austen' }); // { _id: 1, first_name: 'Jane', last_name: 'Austen' }
// collections have autoincremented identifier but accept identifiers already set
// collections have auto incremented identifiers by default but accept identifiers already set
authorsCollection.addOne({ _id: 3, first_name: 'Marcel', last_name: 'Proust' }); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' }
restServer2.addCollection('authors', authorsCollection);
// collections are mutable
authorsCollection.updateOne(1, { last_name: 'Doe' }); // { _id: 1, first_name: 'Jane', last_name: 'Doe' }
authorsCollection.removeOne(3); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' }

var server = sinon.fakeServer.create();
const server = sinon.fakeServer.create();
server.autoRespond = true;
server.respondWith(restServer.getHandler());
server.respondWith(restServer2.getHandler());
```

## Configure Identifiers Generation

By default, FakeRest uses an auto incremented sequence for the items identifiers. If you'd rather uses UUID for instance but would like to avoid providing them when you insert new items, you can provide your own function:

```js
import FakeRest from 'fakerest';
import uuid from 'uuid';

const restServer = new FakeRest.Server('http://my.custom.domain', () => uuid.v5());
```

This can also be specified at the collection level:

```js
import FakeRest from 'fakerest';
import uuid from 'uuid';

const restServer = new FakeRest.Server('http://my.custom.domain');
const authorsCollection = new FakeRest.Collection([], '_id', , () => uuid.v5());
```

## Development

```sh
Expand Down
17 changes: 11 additions & 6 deletions src/BaseServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ export class BaseServer {
batchUrl: string | null = null;
collections: Record<string, Collection<any>> = {};
singles: Record<string, Single<any>> = {};
getNewId?: () => number | string;

constructor(baseUrl = '') {
constructor(baseUrl = '', getNewId?: () => number | string) {
this.baseUrl = baseUrl;
this.getNewId = getNewId;
}

/**
Expand All @@ -21,7 +23,10 @@ export class BaseServer {
for (const name in data) {
const value = data[name];
if (Array.isArray(value)) {
this.addCollection(name, new Collection(value, 'id'));
this.addCollection(
name,
new Collection(value, 'id', this.getNewId),
);
} else {
this.addSingle(name, new Single(value));
}
Expand Down Expand Up @@ -103,25 +108,25 @@ export class BaseServer {
return this.collections[name].getAll(params);
}

getOne(name: string, identifier: number, params?: Query) {
getOne(name: string, identifier: string | number, params?: Query) {
return this.collections[name].getOne(identifier, params);
}

addOne(name: string, item: CollectionItem) {
if (!Object.prototype.hasOwnProperty.call(this.collections, name)) {
this.addCollection(
name,
new Collection([] as CollectionItem[], 'id'),
new Collection([] as CollectionItem[], 'id', this.getNewId),
);
}
return this.collections[name].addOne(item);
}

updateOne(name: string, identifier: number, item: CollectionItem) {
updateOne(name: string, identifier: string | number, item: CollectionItem) {
return this.collections[name].updateOne(identifier, item);
}

removeOne(name: string, identifier: number) {
removeOne(name: string, identifier: string | number) {
return this.collections[name].removeOne(identifier);
}

Expand Down
50 changes: 50 additions & 0 deletions src/Collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,4 +995,54 @@ describe('Collection', () => {
expect(r.id).toEqual(2);
});
});

describe('custom identifier generation', () => {
test('should use the custom identifier provided at initialization', () => {
const collection = new Collection(
[
{
id: '6090eb22-e140-4720-b7b2-e1416a3d2447',
name: 'foo',
},
{
id: 'fb1c2ce1-5df7-4af8-be1c-7af234b67f7d',
name: 'baz',
},
],
'id',
);

expect(
collection.getOne('6090eb22-e140-4720-b7b2-e1416a3d2447'),
).toEqual({
id: '6090eb22-e140-4720-b7b2-e1416a3d2447',
name: 'foo',
});
});

test('should use the custom identifier provided at insertion', () => {
const collection = new Collection<CollectionItem>([], 'id');

const item = collection.addOne({
id: '6090eb22-e140-4720-b7b2-e1416a3d2447',
name: 'foo',
});

expect(item.id).toEqual('6090eb22-e140-4720-b7b2-e1416a3d2447');
});

test('should use the custom identifier generation function at insertion', () => {
const collection = new Collection<CollectionItem>(
[],
'id',
() => '6090eb22-e140-4720-b7b2-e1416a3d2447',
);

const item = collection.addOne({
name: 'foo',
});

expect(item.id).toEqual('6090eb22-e140-4720-b7b2-e1416a3d2447');
});
});
});
33 changes: 20 additions & 13 deletions src/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ export class Collection<T extends CollectionItem = CollectionItem> {
server: BaseServer | null = null;
name: string | null = null;
identifierName = 'id';
getNewId: () => number | string;

constructor(items: T[] = [], identifierName = 'id') {
constructor(
items: T[] = [],
identifierName = 'id',
getNewId?: () => number | string,
) {
if (!Array.isArray(items)) {
throw new Error(
"Can't initialize a Collection with anything else than an array of items",
);
}
this.identifierName = identifierName;
this.getNewId = getNewId || this.getNewIdFromSequence;
items.map(this.addOne.bind(this));
}

Expand Down Expand Up @@ -160,14 +166,14 @@ export class Collection<T extends CollectionItem = CollectionItem> {
return items;
}

getIndex(identifier: number) {
getIndex(identifier: number | string) {
return this.items.findIndex(
// biome-ignore lint/suspicious/noDoubleEquals: we want implicit type coercion
(item) => item[this.identifierName] == identifier,
);
}

getOne(identifier: number, query?: Query) {
getOne(identifier: number | string, query?: Query) {
const index = this.getIndex(identifier);
if (index === -1) {
throw new Error(`No item with identifier ${identifier}`);
Expand All @@ -180,29 +186,30 @@ export class Collection<T extends CollectionItem = CollectionItem> {
return item;
}

getNewIdFromSequence() {
return this.sequence++;
}

addOne(item: T) {
const identifier = item[this.identifierName];
if (identifier != null && typeof identifier !== 'number') {
throw new Error(
`Item must have an identifier of type number, got ${typeof identifier}`,
);
}
if (identifier != null) {
if (this.getIndex(identifier) !== -1) {
throw new Error(
`An item with the identifier ${identifier} already exists`,
);
}
this.sequence = Math.max(this.sequence, identifier) + 1;
if (typeof identifier === 'number') {
this.sequence = Math.max(this.sequence, identifier) + 1;
}
} else {
// @ts-expect-error - For some reason, TS does not accept writing a generic types with the index signature
item[this.identifierName] = this.sequence++;
item[this.identifierName] = this.getNewId();
}
this.items.push(item);
return Object.assign({}, item); // clone item to avoid returning the original;
}

updateOne(identifier: number, item: T) {
updateOne(identifier: number | string, item: T) {
const index = this.getIndex(identifier);
if (index === -1) {
throw new Error(`No item with identifier ${identifier}`);
Expand All @@ -213,15 +220,15 @@ export class Collection<T extends CollectionItem = CollectionItem> {
return Object.assign({}, this.items[index]); // clone item to avoid returning the original
}

removeOne(identifier: number) {
removeOne(identifier: number | string) {
const index = this.getIndex(identifier);
if (index === -1) {
throw new Error(`No item with identifier ${identifier}`);
}
const item = this.items[index];
this.items.splice(index, 1);
// biome-ignore lint/suspicious/noDoubleEquals: we want implicit type coercion
if (identifier == this.sequence - 1) {
if (typeof identifier === 'number' && identifier == this.sequence - 1) {
this.sequence--;
}
return item;
Expand Down
4 changes: 0 additions & 4 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ export class Server extends BaseServer {
requestInterceptors: SinonRequestInterceptor[] = [];
responseInterceptors: SinonResponseInterceptor[] = [];

constructor(baseUrl = '') {
super(baseUrl);
}

addRequestInterceptor(interceptor: SinonRequestInterceptor) {
this.requestInterceptors.push(interceptor);
}
Expand Down

0 comments on commit 51de8e4

Please sign in to comment.