-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(repository): add KVRepository impl using legacy juggler #1539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| kvModelClass: typeof juggler.KeyValueModel; | ||
|
|
||
| constructor(kvModelClass: typeof juggler.KeyValueModel) { | ||
| this.kvModelClass = kvModelClass; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this design is not correct. IIRC, in order to create juggler.KeyValueModel, one needs a DataSource (or at least a ModelBuilder). That's incompatible with LB4 design, where model classes are standalone and decoupled from repository implementation (legacy juggler bridge or other).
See how DefaultCrudRepository takes a LB4 model definitions and creates a backing LB3 PersistedModel instance:
IMO, we should do the same for KeyValue models too.
| } | ||
|
|
||
| get(key: string, options?: Options): Promise<T> { | ||
| const val = this.kvModelClass.get(key, options) as legacy.PromiseOrVoid<T>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a valid cast. Typically, T is a subclass of the new LB4 Model class, while kvModelClass.get returns an instance of legacy LB3 KeyValueModel class. (See also my comment above.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a valid cast. Typically,
Tis a subclass of the new LB4Modelclass, whilekvModelClass.getreturns an instance of legacy LB3KeyValueModelclass. (See also my comment above.)
☝️ this is an important point to address or discuss ☝️
You can take a look at how (CRUD) Repository deals with conversion between backing-model instances and LB4 model instances.
| return ensurePromise<string[]>(this.kvModelClass.keys(filter, options)); | ||
| } | ||
|
|
||
| iterateKeys(filter?: KVFilter, options?: Options): AsyncIterator<string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the previous pull request, I proposed to drop keys(...): Promise<string[]> and rename iterateKeys to keys. What are your arguments for keeping both flavors?
Cross-posting #1527 (comment):
Loading all keys into memory is a foot-gun that opens doors to a DoS attack where the datasource is queried with a pattern that matches too many keys.
The only reason why I added the method keys(): string[], was to work around the limitations of LB 3.x - the way how it was directly exposing juggler methods via REST APIs and a lack of support for streaming responses.
We don't have that limitation in LB4 because REST API is exposed via custom Controller classes that can work around missing support for streaming responses in LB4.
I am proposing to remove keys(): string[] API from LB4 and provide only one method based on AsyncIterator. Ideally, this single method should be called keys instead of iterateKeys for simplicity.
Until our REST layer supports AsyncIterators, we can provide a helper function to convert AsyncIterator into a static array that people can use in their controllers.
Thoughts?
I am ok to leave this change out of this pull request if you prefer, as long as we make it happen before the first public release of KeyValue feature in LB4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree.
| name: 'db', | ||
| connector: 'kv-memory', | ||
| }); | ||
| kvNoteModel = ds.createModel<typeof juggler.KeyValueModel>('note'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, users should not need to understand these low-level implementation details of the legacy juggler bridge. Instead, they should be able to use the same API as DefaultCrudRepository offers now.
repo = new DefaultKVRepository<Note>(Note, ds);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1.
| const note1 = {title: 't1', content: 'c1'}; | ||
| await repo.set('note1', note1); | ||
| const result = await repo.get('note1'); | ||
| expect(result).to.eql(note1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an assertion that result is instanceof Note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an assertion that
resultisinstanceof Note.
PTAL ☝️
| await repo.set('note2', {title: 't2', content: 'c2'}); | ||
| const keys = repo.iterateKeys!(); | ||
| const keyList: string[] = []; | ||
| while (true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered using for-await-of? I believe TS should already support it when the lib is set to es2018.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, esnext is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we need to return an iteratable object (with a Symbol.asyncIterator property) instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we need to return an iteratable object (with a Symbol.asyncIterator property) instead.
Let's do that please. It's one of the main reasons why I am pushing for AsyncIterator interface - to allow people to consume the keys via for-await-of or any async-iterable helper functions (think of lodash for async-iterables).
IIRC, esnext is needed.
You are right. I am proposing to add ESNext.AsyncIterable to our lib. Async iterators are already available on Node.js 10.0.0 with no feature flags needed.
30ce60e to
0b9e47b
Compare
1fdaa93 to
ca93e61
Compare
| "strictNullChecks": true, | ||
|
|
||
| "lib": ["es2018", "dom"], | ||
| "lib": ["es2018", "dom", "esnext.asynciterable"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| } | ||
|
|
||
| get(key: string, options?: Options): Promise<T> { | ||
| const val = this.kvModelClass.get(key, options) as legacy.PromiseOrVoid<T>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a valid cast. Typically,
Tis a subclass of the new LB4Modelclass, whilekvModelClass.getreturns an instance of legacy LB3KeyValueModelclass. (See also my comment above.)
☝️ this is an important point to address or discuss ☝️
You can take a look at how (CRUD) Repository deals with conversion between backing-model instances and LB4 model instances.
| * Polyfill for AsyncIterator before es.next is ready | ||
| */ | ||
| // tslint:disable:no-any | ||
| export interface AsyncIterator<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this poly-fill after you enabled esnext.asynciterable in tsconfig?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
| const note1 = {title: 't1', content: 'c1'}; | ||
| await repo.set('note1', note1); | ||
| const result = await repo.get('note1'); | ||
| expect(result).to.eql(note1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an assertion that
resultisinstanceof Note.
PTAL ☝️
| return ensurePromise<number>(this.kvModelClass.ttl(key, options)); | ||
| } | ||
|
|
||
| keys(filter?: KVFilter, options?: Options): Promise<string[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the thread #1539 (comment), you said you agree to drop iterateKeys and rework keys to return an async iterator. However, I see the old API is still present. Perhaps an oversight?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| /** | ||
| * An implementation of KVRepository based on loopback-datasource-juggler | ||
| */ | ||
| export class DefaultKVRepository<T extends Model> implements KVRepository<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you are touching these APIs, could you please rename KV to KeyValue for more clarity? DefaultKeyValueRepository and KeyValueRepository.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed
12b58c1 to
b41b17e
Compare
b41b17e to
8ba0801
Compare
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two more comments, feel free to address them in a follow-up pull request.
| // KVModel class is placeholder to receive methods from KeyValueAccessObject | ||
| // through mixin | ||
| this.kvModelClass = ds.createModel<typeof juggler.KeyValueModel>( | ||
| '_kvModel', |
There was a problem hiding this comment.
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.
| const note1 = {title: 't1', content: 'c1'}; | ||
| await repo.set('note1', note1); | ||
| const result = await repo.get('note1'); | ||
| expect(result).to.eql(new Note(note1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is eql comparing prototype/constructor? I would prefer an explicit instanceof check instead.
expect(result).to.be.instanceOf(Note);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Replacing #1527, see also #1448
Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated