diff --git a/packages/build/config/tsconfig.common.json b/packages/build/config/tsconfig.common.json index 9df7437a80c4..641d3bf4f7de 100644 --- a/packages/build/config/tsconfig.common.json +++ b/packages/build/config/tsconfig.common.json @@ -6,7 +6,7 @@ "noImplicitAny": true, "strictNullChecks": true, - "lib": ["es2018", "dom"], + "lib": ["es2018", "dom", "esnext.asynciterable"], "module": "commonjs", "moduleResolution": "node", "target": "es2017", diff --git a/packages/repository/examples/juggler-bridge/note-with-repo-class.ts b/packages/repository/examples/juggler-bridge/note-with-repo-class.ts index 80167002778c..5db8805c2fdf 100644 --- a/packages/repository/examples/juggler-bridge/note-with-repo-class.ts +++ b/packages/repository/examples/juggler-bridge/note-with-repo-class.ts @@ -14,11 +14,6 @@ import { ModelDefinition, } from '../../'; -class NoteController { - @repository('noteRepo') - public noteRepo: EntityCrudRepository; -} - const ds: juggler.DataSource = new juggler.DataSource({ name: 'db', connector: 'memory', @@ -29,6 +24,14 @@ class Note extends Entity { name: 'note', properties: {title: 'string', content: 'string'}, }); + + title: string; + content?: string; +} + +class NoteController { + @repository('noteRepo') + public noteRepo: EntityCrudRepository; } class MyNoteRepository extends DefaultCrudRepository { diff --git a/packages/repository/examples/juggler-bridge/note-with-repo-instance.ts b/packages/repository/examples/juggler-bridge/note-with-repo-instance.ts index 5fa41ad73365..151c224c3bdb 100644 --- a/packages/repository/examples/juggler-bridge/note-with-repo-instance.ts +++ b/packages/repository/examples/juggler-bridge/note-with-repo-instance.ts @@ -16,14 +16,28 @@ import { ModelDefinition, } from '../../'; +const ds: juggler.DataSource = new juggler.DataSource({ + name: 'db', + connector: 'memory', +}); + +class Note extends Entity { + static definition = new ModelDefinition({ + name: 'note', + properties: {title: 'string', content: 'string'}, + }); + + title: string; + content?: string; +} + // The Controller for Note class NoteController { constructor( - @repository('noteRepo') - public noteRepo: EntityCrudRepository, + @repository('noteRepo') public noteRepo: EntityCrudRepository, ) {} - create(data: DataObject, options?: Options) { + create(data: DataObject, options?: Options) { return this.noteRepo.create(data, options); } @@ -32,18 +46,6 @@ class NoteController { } } -const ds: juggler.DataSource = new juggler.DataSource({ - name: 'db', - connector: 'memory', -}); - -class Note extends Entity { - static definition = new ModelDefinition({ - name: 'note', - properties: {title: 'string', content: 'string'}, - }); -} - async function main() { // Create a context const ctx = new Context(); diff --git a/packages/repository/package.json b/packages/repository/package.json index 3ff65878d90a..83b598d3eeae 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -32,7 +32,7 @@ "@loopback/core": "^0.11.3", "@loopback/dist-util": "^0.3.6", "lodash": "^4.17.10", - "loopback-datasource-juggler": "^3.22.1" + "loopback-datasource-juggler": "^3.23.0" }, "files": [ "README.md", diff --git a/packages/repository/src/common-types.ts b/packages/repository/src/common-types.ts index 3cf2b61ded01..da1ca039c1c2 100644 --- a/packages/repository/src/common-types.ts +++ b/packages/repository/src/common-types.ts @@ -44,9 +44,15 @@ export interface AnyObject { } /** - * Type alias for T or any object + * An extension of the built-in Partial type which allows partial values + * in deeply nested properties too. */ -export type DataObject = T | AnyObject; +export type DeepPartial = {[P in keyof T]?: DeepPartial}; + +/** + * Type alias for strongly or weakly typed objects of T + */ +export type DataObject = T | DeepPartial; /** * Type alias for Node.js options object diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index 89ec260d9c6c..10acd085baee 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -178,7 +178,7 @@ export abstract class Model { return obj; } - constructor(data?: Partial) { + constructor(data?: DataObject) { Object.assign(this, data); } } diff --git a/packages/repository/src/repositories/constraint-utils.ts b/packages/repository/src/repositories/constraint-utils.ts index f81f08918633..dc15a5c60aae 100644 --- a/packages/repository/src/repositories/constraint-utils.ts +++ b/packages/repository/src/repositories/constraint-utils.ts @@ -51,7 +51,7 @@ export function constrainWhere( */ export function constrainDataObject( originalData: DataObject, - constraint: Partial, + constraint: DataObject, ): DataObject { const constrainedData = cloneDeep(originalData); for (const c in constraint) { diff --git a/packages/repository/src/repositories/index.ts b/packages/repository/src/repositories/index.ts index 6550183558c6..14492905861e 100644 --- a/packages/repository/src/repositories/index.ts +++ b/packages/repository/src/repositories/index.ts @@ -5,6 +5,7 @@ export * from './kv.repository'; export * from './legacy-juggler-bridge'; +export * from './kv.repository.bridge'; export * from './repository'; export * from './relation.factory'; export * from './relation.repository'; diff --git a/packages/repository/src/repositories/kv.repository.bridge.ts b/packages/repository/src/repositories/kv.repository.bridge.ts new file mode 100644 index 000000000000..807f202a5c19 --- /dev/null +++ b/packages/repository/src/repositories/kv.repository.bridge.ts @@ -0,0 +1,108 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as legacy from 'loopback-datasource-juggler'; + +import {Options, DataObject} from '../common-types'; +import {Entity} from '../model'; + +import {KeyValueRepository, KeyValueFilter} from './kv.repository'; + +import {juggler, ensurePromise} from './legacy-juggler-bridge'; + +/** + * Polyfill for Symbol.asyncIterator + * See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html + */ +// tslint:disable-next-line:no-any +if (!(Symbol as any).asyncIterator) { + // tslint:disable-next-line:no-any + (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator'); +} + +/** + * An implementation of KeyValueRepository based on loopback-datasource-juggler + */ +export class DefaultKeyValueRepository + implements KeyValueRepository { + /** + * A legacy KeyValueModel class + */ + kvModelClass: typeof juggler.KeyValueModel; + + /** + * Construct a KeyValueRepository with a legacy DataSource + * @param ds Legacy DataSource + */ + constructor( + private entityClass: typeof Entity & {prototype: T}, + ds: juggler.DataSource, + ) { + // KVModel class is placeholder to receive methods from KeyValueAccessObject + // through mixin + this.kvModelClass = ds.createModel( + '_kvModel', + ); + } + + delete(key: string, options?: Options): Promise { + return ensurePromise(this.kvModelClass.delete(key, options)); + } + + deleteAll(options?: Options): Promise { + return ensurePromise(this.kvModelClass.deleteAll(options)); + } + + protected toEntity(modelData: legacy.ModelData): T { + if (modelData == null) return modelData; + let data = modelData; + if (typeof modelData.toObject === 'function') { + data = modelData.toObject(); + } + return new this.entityClass(data) as T; + } + + async get(key: string, options?: Options): Promise { + const val = this.kvModelClass.get(key, options) as legacy.PromiseOrVoid< + legacy.ModelData + >; + const result = await ensurePromise(val); + return this.toEntity(result); + } + + set(key: string, value: DataObject, options?: Options): Promise { + return ensurePromise(this.kvModelClass.set(key, value, options)); + } + + expire(key: string, ttl: number, options?: Options): Promise { + return ensurePromise(this.kvModelClass.expire(key, ttl, options)); + } + + ttl(key: string, options?: Options): Promise { + return ensurePromise(this.kvModelClass.ttl(key, options)); + } + + keys(filter?: KeyValueFilter, options?: Options): AsyncIterable { + const kvModelClass = this.kvModelClass; + const iterator = { + [Symbol.asyncIterator]() { + return new AsyncKeyIteratorImpl( + kvModelClass.iterateKeys(filter, options), + ); + }, + }; + return iterator; + } +} + +class AsyncKeyIteratorImpl implements AsyncIterator { + constructor(private keys: legacy.AsyncKeyIterator) {} + next() { + const key = ensurePromise(this.keys.next()); + return key.then(k => { + return {done: k === undefined, value: k || ''}; + }); + } +} diff --git a/packages/repository/src/repositories/kv.repository.ts b/packages/repository/src/repositories/kv.repository.ts index 57af27c65197..95afc7bd5ad6 100644 --- a/packages/repository/src/repositories/kv.repository.ts +++ b/packages/repository/src/repositories/kv.repository.ts @@ -6,30 +6,38 @@ import {Repository} from './repository'; import {Options, DataObject} from '../common-types'; import {Model} from '../model'; -import {Filter} from '../query'; + +/** + * Filter for keys + */ +export type KeyValueFilter = { + /** + * Glob string to use to filter returned keys (i.e. `userid.*`). All + * connectors are required to support `*` and `?`. They may also support + * additional special characters that are specific to the backing database. + */ + match: string; +}; /** * Key/Value operations for connector implementations */ -export interface KVRepository extends Repository { +export interface KeyValueRepository extends Repository { /** * Delete an entry by key * * @param key Key for the entry * @param options Options for the operation - * @returns Promise if an entry is deleted for the key, otherwise - * Promise */ - delete(key: string, options?: Options): Promise; + delete(key: string, options?: Options): Promise; /** * Delete all entries * * @param key Key for the entry * @param options Options for the operation - * @returns A promise of the number of entries deleted */ - deleteAll(options?: Options): Promise; + deleteAll(options?: Options): Promise; /** * Get an entry by key @@ -46,20 +54,16 @@ export interface KVRepository extends Repository { * @param key Key for the entry * @param value Value for the entry * @param options Options for the operation - * @returns Promise if an entry is set for the key, otherwise - * Promise */ - set(key: string, value: DataObject, options?: Options): Promise; + set(key: string, value: DataObject, options?: Options): Promise; /** * Set up ttl for an entry by key * * @param key Key for the entry * @param options Options for the operation - * @returns Promise if an entry is set for the key, otherwise - * Promise */ - expire(key: string, ttl: number, options?: Options): Promise; + expire(key: string, ttl: number, options?: Options): Promise; /** * Get ttl for an entry by key @@ -68,23 +72,15 @@ export interface KVRepository extends Repository { * @param options Options for the operation * @returns A promise of the TTL value */ - ttl?(key: string, ttl: number, options?: Options): Promise; - - /** - * Fetch all keys - * - * @param key Key for the entry - * @param options Options for the operation - * @returns A promise of an array of keys for all entries - */ - keys?(options?: Options): Promise; + ttl?(key: string, options?: Options): Promise; /** * Get an Iterator for matching keys * * @param filter Filter for keys * @param options Options for the operation - * @returns A promise of an iterator of entries + * @returns An async iteratable iterator of keys so that the return value can + * be used with `for-await-of`. */ - iterateKeys?(filter?: Filter, options?: Options): Promise>; + keys?(filter?: KeyValueFilter, options?: Options): AsyncIterable; } diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 2aa37af20bc1..8b5732239aaa 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -13,6 +13,7 @@ import { Command, NamedParameters, PositionalParameters, + DataObject, } from '../common-types'; import {Entity, ModelDefinition} from '../model'; import {Filter, Where} from '../query'; @@ -31,6 +32,7 @@ export namespace juggler { export import ModelBase = legacy.ModelBase; export import ModelBaseClass = legacy.ModelBaseClass; export import PersistedModel = legacy.PersistedModel; + export import KeyValueModel = legacy.KeyValueModel; export import PersistedModelClass = legacy.PersistedModelClass; } @@ -55,7 +57,7 @@ export function bindModel( * @param p Promise or void */ /* tslint:disable-next-line:no-any */ -function ensurePromise(p: legacy.PromiseOrVoid): Promise { +export function ensurePromise(p: legacy.PromiseOrVoid): Promise { if (p && isPromiseLike(p)) { // Juggler uses promise-like Bluebird instead of native Promise // implementation. We need to convert the promise returned by juggler @@ -172,12 +174,12 @@ export class DefaultCrudRepository ); } - async create(entity: Partial, options?: Options): Promise { + async create(entity: DataObject, options?: Options): Promise { const model = await ensurePromise(this.modelClass.create(entity, options)); return this.toEntity(model); } - async createAll(entities: Partial[], options?: Options): Promise { + async createAll(entities: DataObject[], options?: Options): Promise { const models = await ensurePromise( this.modelClass.create(entities, options), ); @@ -226,7 +228,7 @@ export class DefaultCrudRepository } updateAll( - data: Partial, + data: DataObject, where?: Where, options?: Options, ): Promise { @@ -236,14 +238,18 @@ export class DefaultCrudRepository ); } - updateById(id: ID, data: Partial, options?: Options): Promise { + updateById(id: ID, data: DataObject, options?: Options): Promise { const idProp = this.modelClass.definition.idName(); const where = {} as Where; where[idProp] = id; return this.updateAll(data, where, options).then(count => count > 0); } - replaceById(id: ID, data: Partial, options?: Options): Promise { + replaceById( + id: ID, + data: DataObject, + options?: Options, + ): Promise { return ensurePromise(this.modelClass.replaceById(id, data, options)).then( result => !!result, ); diff --git a/packages/repository/src/repositories/relation.factory.ts b/packages/repository/src/repositories/relation.factory.ts index e466ca32c837..1b89d8d4b227 100644 --- a/packages/repository/src/repositories/relation.factory.ts +++ b/packages/repository/src/repositories/relation.factory.ts @@ -10,6 +10,7 @@ import { HasManyRepository, DefaultHasManyEntityCrudRepository, } from './relation.repository'; +import {DataObject} from '..'; export type HasManyRepositoryFactory = ( fkValue: ForeignKeyType, @@ -43,10 +44,12 @@ export function createHasManyRepositoryFactory< 'The foreign key property name (keyTo) must be specified', ); } + // tslint:disable-next-line:no-any + const constraint: any = {[fkName]: fkValue}; return new DefaultHasManyEntityCrudRepository< Target, TargetID, EntityCrudRepository - >(targetRepository, {[fkName]: fkValue}); + >(targetRepository, constraint as DataObject); }; } diff --git a/packages/repository/src/repositories/relation.repository.ts b/packages/repository/src/repositories/relation.repository.ts index 7914ff0318bf..b9e255dc5552 100644 --- a/packages/repository/src/repositories/relation.repository.ts +++ b/packages/repository/src/repositories/relation.repository.ts @@ -9,7 +9,7 @@ import { constrainFilter, constrainWhere, } from './constraint-utils'; -import {DataObject, AnyObject, Options} from '../common-types'; +import {DataObject, Options} from '../common-types'; import {Entity} from '../model'; import {Filter, Where} from '../query'; @@ -23,7 +23,10 @@ export interface HasManyRepository { * @param options Options for the operation * @returns A promise which resolves to the newly created target model instance */ - create(targetModelData: Partial, options?: Options): Promise; + create( + targetModelData: DataObject, + options?: Options, + ): Promise; /** * Find target model instance(s) * @param Filter A filter object for where, order, limit, etc. @@ -65,11 +68,11 @@ export class DefaultHasManyEntityCrudRepository< */ constructor( public targetRepository: TargetRepository, - public constraint: AnyObject, + public constraint: DataObject, ) {} async create( - targetModelData: Partial, + targetModelData: DataObject, options?: Options, ): Promise { return await this.targetRepository.create( @@ -93,7 +96,7 @@ export class DefaultHasManyEntityCrudRepository< } async patch( - dataObject: Partial, + dataObject: DataObject, where?: Where, options?: Options, ): Promise { diff --git a/packages/repository/test/unit/repositories/kv.repository.bridge.unit.ts b/packages/repository/test/unit/repositories/kv.repository.bridge.unit.ts new file mode 100644 index 000000000000..2d89b9607ee6 --- /dev/null +++ b/packages/repository/test/unit/repositories/kv.repository.bridge.unit.ts @@ -0,0 +1,99 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; + +import { + juggler, + DefaultKeyValueRepository, + DataObject, + KeyValueRepository, + Entity, +} from '../../../'; + +describe('DefaultKeyValueRepository', () => { + let ds: juggler.DataSource; + let repo: KeyValueRepository; + + class Note extends Entity { + title?: string; + content?: string; + id: number; + + constructor(data: DataObject) { + super(data); + } + } + + beforeEach(() => { + ds = new juggler.DataSource({ + name: 'db', + connector: 'kv-memory', + }); + repo = new DefaultKeyValueRepository(Note, ds); + }); + + it('implements KeyValueRepository.set()', async () => { + const note1 = {title: 't1', content: 'c1'}; + await repo.set('note1', note1); + const result = await repo.get('note1'); + expect(result).to.eql(new Note(note1)); + }); + + it('implements KeyValueRepository.get() for non-existent key', async () => { + const result = await repo.get('note1'); + expect(result).be.null(); + }); + + it('implements KeyValueRepository.delete()', async () => { + const note1 = {title: 't1', content: 'c1'}; + await repo.set('note1', note1); + await repo.delete('note1'); + const result = await repo.get('note1'); + expect(result).be.null(); + }); + + it('implements KeyValueRepository.deleteAll()', async () => { + await repo.set('note1', {title: 't1', content: 'c1'}); + await repo.set('note2', {title: 't2', content: 'c2'}); + await repo.deleteAll(); + let result = await repo.get('note1'); + expect(result).be.null(); + result = await repo.get('note2'); + expect(result).be.null(); + }); + + it('implements KeyValueRepository.ttl()', async () => { + await repo.set('note1', {title: 't1', content: 'c1'}, {ttl: 100}); + const result = await repo.ttl!('note1'); + // The remaining ttl <= original ttl + expect(result).to.be.lessThanOrEqual(100); + }); + + it('reports error from KeyValueRepository.ttl()', async () => { + const p = repo.ttl!('note2'); + return expect(p).to.be.rejectedWith( + 'Cannot get TTL for unknown key "note2"', + ); + }); + + it('implements KeyValueRepository.expire()', async () => { + await repo.set('note1', {title: 't1', content: 'c1'}, {ttl: 100}); + await repo.expire!('note1', 200); + const ttl = await repo.ttl!('note1'); + expect(ttl).to.lessThanOrEqual(200); + }); + + it('implements KeyValueRepository.keys()', async () => { + await repo.set('note1', {title: 't1', content: 'c1'}); + await repo.set('note2', {title: 't2', content: 'c2'}); + const keys = repo.keys!(); + const keyList: string[] = []; + for await (const k of keys) { + keyList.push(k); + } + expect(keyList).to.eql(['note1', 'note2']); + }); +}); diff --git a/packages/repository/test/unit/repositories/relation.repository.unit.ts b/packages/repository/test/unit/repositories/relation.repository.unit.ts index f70ec0f54106..5dec448cd672 100644 --- a/packages/repository/test/unit/repositories/relation.repository.unit.ts +++ b/packages/repository/test/unit/repositories/relation.repository.unit.ts @@ -32,7 +32,7 @@ describe('relation repository', () => { TargetRepository extends EntityCrudRepository > implements HasManyRepository { create( - targetModelData: Partial, + targetModelData: DataObject, options?: AnyObject | undefined, ): Promise { /* istanbul ignore next */ @@ -128,7 +128,7 @@ describe('relation repository', () => { let repo: CustomerRepository; - function givenDefaultHasManyCrudInstance(constraint: AnyObject) { + function givenDefaultHasManyCrudInstance(constraint: DataObject) { repo = sinon.createStubInstance(CustomerRepository); return new DefaultHasManyEntityCrudRepository< Customer, diff --git a/packages/service-proxy/package.json b/packages/service-proxy/package.json index ca9376b9205a..08d5d5502ca0 100644 --- a/packages/service-proxy/package.json +++ b/packages/service-proxy/package.json @@ -34,7 +34,7 @@ "@loopback/context": "^0.12.3", "@loopback/core": "^0.11.3", "@loopback/dist-util": "^0.3.6", - "loopback-datasource-juggler": "^3.20.2" + "loopback-datasource-juggler": "^3.23.0" }, "files": [ "README.md",