Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/build/config/tsconfig.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"noImplicitAny": true,
"strictNullChecks": true,

"lib": ["es2018", "dom"],
"lib": ["es2018", "dom", "esnext.asynciterable"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"module": "commonjs",
"moduleResolution": "node",
"target": "es2017",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ import {
ModelDefinition,
} from '../../';

class NoteController {
@repository('noteRepo')
public noteRepo: EntityCrudRepository<Entity, number>;
}

const ds: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
Expand All @@ -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<Note, number>;
}

class MyNoteRepository extends DefaultCrudRepository<Note, string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity, number>,
@repository('noteRepo') public noteRepo: EntityCrudRepository<Note, number>,
) {}

create(data: DataObject<Entity>, options?: Options) {
create(data: DataObject<Note>, options?: Options) {
return this.noteRepo.create(data, options);
}

Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion packages/repository/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions packages/repository/src/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ export interface AnyObject {
}

/**
* Type alias for T or any object
* An extension of the built-in Partial<T> type which allows partial values
* in deeply nested properties too.
*/
export type DataObject<T> = T | AnyObject;
export type DeepPartial<T> = {[P in keyof T]?: DeepPartial<T[P]>};

/**
* Type alias for strongly or weakly typed objects of T
*/
export type DataObject<T> = T | DeepPartial<T>;

/**
* Type alias for Node.js options object
Expand Down
2 changes: 1 addition & 1 deletion packages/repository/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export abstract class Model {
return obj;
}

constructor(data?: Partial<Model>) {
constructor(data?: DataObject<Model>) {
Object.assign(this, data);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/repository/src/repositories/constraint-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function constrainWhere(
*/
export function constrainDataObject<T extends Entity>(
originalData: DataObject<T>,
constraint: Partial<T>,
constraint: DataObject<T>,
): DataObject<T> {
const constrainedData = cloneDeep(originalData);
for (const c in constraint) {
Expand Down
1 change: 1 addition & 0 deletions packages/repository/src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
108 changes: 108 additions & 0 deletions packages/repository/src/repositories/kv.repository.bridge.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Entity>
implements KeyValueRepository<T> {
/**
* 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<typeof juggler.KeyValueModel>(
'_kvModel',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote a comment on this line that was somehow lost :(

Consider the following scenario: an app has two key-value models called Stats and ShoppingCart, and two KV repository classes extending DefaultKeyValueRepository, both using the same backing datasources. The first instance will define _kvModel model, the second instance will re-define this model again. From what I remember from LB 3.x days, redefining model of the same name was considering as invalid usage of juggler API.

Creating a new model class on every request has negative performance impact too.

Another problem I see here is that we are not defining any properties on the backing model class. As a result, legacy juggler is not going to verify that the input data are valid model properties. AFAIK, KVAO does not implement validations yet (see lib/kvao/set.js), thus I guess this second point is not a blocker for this pull request.

Ideally, I'd like our DefaultKeyValueRepository to leverage the same mechanism that DefaultCrudRepository uses:

  • The backing model has the same name as the LB4 model.
  • If the datasource already has a backing model defined, then we reuse it.
  • When defining a new backing model, LB4 property definition is converted to juggler schema.

See https://github.com/strongloop/loopback-next/blob/5a6bed821fa01433c48ad437578d5a13f8a11cd8/packages/repository/src/repositories/legacy-juggler-bridge.ts#L101-L132

);
}

delete(key: string, options?: Options): Promise<void> {
return ensurePromise(this.kvModelClass.delete(key, options));
}

deleteAll(options?: Options): Promise<void> {
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<T> {
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<T>, options?: Options): Promise<void> {
return ensurePromise<void>(this.kvModelClass.set(key, value, options));
}

expire(key: string, ttl: number, options?: Options): Promise<void> {
return ensurePromise<void>(this.kvModelClass.expire(key, ttl, options));
}

ttl(key: string, options?: Options): Promise<number> {
return ensurePromise<number>(this.kvModelClass.ttl(key, options));
}

keys(filter?: KeyValueFilter, options?: Options): AsyncIterable<string> {
const kvModelClass = this.kvModelClass;
const iterator = {
[Symbol.asyncIterator]() {
return new AsyncKeyIteratorImpl(
kvModelClass.iterateKeys(filter, options),
);
},
};
return iterator;
}
}

class AsyncKeyIteratorImpl implements AsyncIterator<string> {
constructor(private keys: legacy.AsyncKeyIterator) {}
next() {
const key = ensurePromise<string | undefined>(this.keys.next());
return key.then(k => {
return {done: k === undefined, value: k || ''};
});
}
}
46 changes: 21 additions & 25 deletions packages/repository/src/repositories/kv.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Model> extends Repository<T> {
export interface KeyValueRepository<T extends Model> extends Repository<T> {
/**
* Delete an entry by key
*
* @param key Key for the entry
* @param options Options for the operation
* @returns Promise<true> if an entry is deleted for the key, otherwise
* Promise<false>
*/
delete(key: string, options?: Options): Promise<boolean>;
delete(key: string, options?: Options): Promise<void>;

/**
* 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<number>;
deleteAll(options?: Options): Promise<void>;

/**
* Get an entry by key
Expand All @@ -46,20 +54,16 @@ export interface KVRepository<T extends Model> extends Repository<T> {
* @param key Key for the entry
* @param value Value for the entry
* @param options Options for the operation
* @returns Promise<true> if an entry is set for the key, otherwise
* Promise<false>
*/
set(key: string, value: DataObject<T>, options?: Options): Promise<boolean>;
set(key: string, value: DataObject<T>, options?: Options): Promise<void>;

/**
* Set up ttl for an entry by key
*
* @param key Key for the entry
* @param options Options for the operation
* @returns Promise<true> if an entry is set for the key, otherwise
* Promise<false>
*/
expire(key: string, ttl: number, options?: Options): Promise<boolean>;
expire(key: string, ttl: number, options?: Options): Promise<void>;

/**
* Get ttl for an entry by key
Expand All @@ -68,23 +72,15 @@ export interface KVRepository<T extends Model> extends Repository<T> {
* @param options Options for the operation
* @returns A promise of the TTL value
*/
ttl?(key: string, ttl: number, options?: Options): Promise<number>;

/**
* 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<string[]>;
ttl?(key: string, options?: Options): Promise<number>;

/**
* 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<Iterator<T>>;
keys?(filter?: KeyValueFilter, options?: Options): AsyncIterable<string>;
}
Loading