@travetto/model 8.0.0-alpha.2 → 8.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/util/crud.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { castTo, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
1
+ import { castTo, castKey, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
2
2
  import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import { ModelRegistryIndex } from '../registry/registry-index.ts';
@@ -34,6 +34,21 @@ export class ModelCrudUtil {
34
34
  return { create, valid };
35
35
  }
36
36
 
37
+ static async filterOutNotFound<T extends ModelType>(actions: Promise<T>[] | undefined): Promise<T[]> {
38
+ if (!actions) {
39
+ return [];
40
+ }
41
+ return (await Promise.allSettled(actions)).map(p => {
42
+ if (p.status === 'fulfilled') {
43
+ return p.value;
44
+ } else if (p.reason instanceof NotFoundError) {
45
+ return undefined!;
46
+ } else {
47
+ throw p.reason;
48
+ }
49
+ }).filter(item => !!item);
50
+ }
51
+
37
52
  /**
38
53
  * Load model
39
54
  * @param cls Class to load model for
@@ -96,6 +111,70 @@ export class ModelCrudUtil {
96
111
  return castTo(item);
97
112
  }
98
113
 
114
+ /**
115
+ * Recursively deletes or masks transient fields from an object based on its Model config.
116
+ * If transient fields are present, returns a copy with those fields set to undefined.
117
+ */
118
+ static cleanTransientFields<T>(cls: Class<T>, item: T): T {
119
+ if (!item || typeof item !== 'object') {
120
+ return item;
121
+ }
122
+ if (SchemaRegistryIndex.has(cls)) {
123
+ const schema = SchemaRegistryIndex.get(cls).get();
124
+ const transientFields = ModelRegistryIndex.has(cls) ? ModelRegistryIndex.getConfig(cls).transientFields : undefined;
125
+
126
+ const isCleanTarget = (key: string): boolean => {
127
+ return transientFields?.includes(key) ?? false;
128
+ };
129
+
130
+ const hasTargets = Object.keys(schema.fields).some(key => isCleanTarget(key));
131
+
132
+ let res = item;
133
+ if (hasTargets) {
134
+ res = { ...item };
135
+ for (const key of Object.keys(schema.fields)) {
136
+ if (isCleanTarget(key)) {
137
+ res[castKey<T>(key)] = undefined!;
138
+ }
139
+ }
140
+ }
141
+
142
+ for (const [key, field] of Object.entries(schema.fields)) {
143
+ const fieldKey = castKey<typeof res>(key);
144
+ if (!isCleanTarget(key) && res[fieldKey] !== undefined && res[fieldKey] !== null) {
145
+ if (field.array && Array.isArray(res[fieldKey])) {
146
+ const arr = res[fieldKey] as any[];
147
+ let changed = false;
148
+ const newArr = arr.map(subItem => {
149
+ const cleaned = this.cleanTransientFields(field.type, subItem);
150
+ if (cleaned !== subItem) {
151
+ changed = true;
152
+ }
153
+ return cleaned;
154
+ });
155
+ if (changed) {
156
+ if (res === item) {
157
+ res = { ...item };
158
+ }
159
+ res[fieldKey] = newArr as any;
160
+ }
161
+ } else {
162
+ const val = res[fieldKey];
163
+ const cleaned = this.cleanTransientFields(field.type, val);
164
+ if (cleaned !== val) {
165
+ if (res === item) {
166
+ res = { ...item };
167
+ }
168
+ res[fieldKey] = cleaned;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ return res;
174
+ }
175
+ return item;
176
+ }
177
+
99
178
  /**
100
179
  * Ensure subtype is not supported
101
180
  */
@@ -117,10 +196,7 @@ export class ModelCrudUtil {
117
196
  item = await handler(item) ?? item;
118
197
  }
119
198
  }
120
- if (typeof item === 'object' && item && 'prePersist' in item && typeof item['prePersist'] === 'function') {
121
- item = await item.prePersist() ?? item;
122
- }
123
- return item;
199
+ return this.cleanTransientFields(cls, item);
124
200
  }
125
201
 
126
202
  /**
@@ -131,9 +207,6 @@ export class ModelCrudUtil {
131
207
  for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
132
208
  item = await handler(item) ?? item;
133
209
  }
134
- if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
135
- item = await item.postLoad() ?? item;
136
- }
137
210
  return item;
138
211
  }
139
212
 
@@ -6,7 +6,10 @@ import { ModelExportUtil } from './bin/export.ts';
6
6
  import { ModelCandidateUtil } from './bin/candidate.ts';
7
7
 
8
8
  /**
9
- * Exports model schemas
9
+ * Export model definitions for a selected provider and model set.
10
+ *
11
+ * The command resolves candidate models and delegates to provider-specific
12
+ * export logic to produce schema/install artifacts.
10
13
  */
11
14
  @CliCommand()
12
15
  export class ModelExportCommand extends BaseModelCommand {
@@ -6,7 +6,10 @@ import { ModelInstallUtil } from './bin/install.ts';
6
6
  import { ModelCandidateUtil } from './bin/candidate.ts';
7
7
 
8
8
  /**
9
- * Installing models
9
+ * Install or update model definitions for a selected provider.
10
+ *
11
+ * The command resolves candidate models and applies provider install/upsert
12
+ * operations so backing stores are prepared for runtime usage.
10
13
  */
11
14
  @CliCommand()
12
15
  export class ModelInstallCommand extends BaseModelCommand {
@@ -8,7 +8,6 @@ import type { ModelBlobSupport } from '../src/types/blob.ts';
8
8
  import type { ModelBulkSupport } from '../src/types/bulk.ts';
9
9
  import type { ModelCrudSupport } from '../src/types/crud.ts';
10
10
  import type { ModelExpirySupport } from '../src/types/expiry.ts';
11
- import type { ModelIndexedSupport } from '../src/types/indexed.ts';
12
11
 
13
12
  const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'> =>
14
13
  d.codeLink(title, Runtime.getSourceFile(target), new RegExp(`\\binterface\\s+${target.name}`));
@@ -17,7 +16,6 @@ export const Links = {
17
16
  Basic: toLink('Basic', toConcrete<ModelBasicSupport>()),
18
17
  Crud: toLink('CRUD', toConcrete<ModelCrudSupport>()),
19
18
  Expiry: toLink('Expiry', toConcrete<ModelExpirySupport>()),
20
- Indexed: toLink('Indexed', toConcrete<ModelIndexedSupport>()),
21
19
  Bulk: toLink('Bulk', toConcrete<ModelBulkSupport>()),
22
20
  Blob: toLink('Blob', toConcrete<ModelBlobSupport>()),
23
21
  };
@@ -22,8 +22,8 @@ export abstract class BaseModelSuite<T> {
22
22
  const svc = (await this.service);
23
23
  if (ModelCrudUtil.isSupported(svc)) {
24
24
  let i = 0;
25
- for await (const __el of svc.list(cls)) {
26
- i += 1;
25
+ for await (const batch of svc.list(cls)) {
26
+ i += batch.length;
27
27
  }
28
28
  return i;
29
29
  } else {
@@ -52,11 +52,11 @@ export abstract class BaseModelSuite<T> {
52
52
  return DependencyRegistryIndex.getInstance(this.serviceClass);
53
53
  }
54
54
 
55
- async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
56
- const out: U[] = [];
55
+ async toArray<U>(src: AsyncIterable<U | U[]> | AsyncGenerator<U | U[]>): Promise<U[]> {
56
+ const out: (U | U[])[] = [];
57
57
  for await (const el of src) {
58
58
  out.push(el);
59
59
  }
60
- return out;
60
+ return castTo(out.flat());
61
61
  }
62
62
  }
@@ -1,10 +1,22 @@
1
1
  import assert from 'node:assert';
2
2
 
3
3
  import { Suite, Test } from '@travetto/test';
4
- import { type ModelCrudSupport, Model, NotFoundError } from '@travetto/model';
4
+ import { type ModelCrudSupport, Model, NotFoundError, ModelCrudUtil, TransientField } from '@travetto/model';
5
5
 
6
6
  import { BaseModelSuite } from './base.ts';
7
7
 
8
+ @Model('computed_person')
9
+ class ComputedPerson {
10
+ id: string;
11
+ name: string;
12
+ @TransientField()
13
+ get nameUpper(): string {
14
+ return this.name.toUpperCase();
15
+ }
16
+ @TransientField()
17
+ ignoredField?: string;
18
+ }
19
+
8
20
  @Model('basic_person')
9
21
  class Person {
10
22
  id: string;
@@ -59,4 +71,31 @@ export abstract class ModelBasicSuite extends BaseModelSuite<ModelCrudSupport> {
59
71
  assert(single !== undefined);
60
72
  assert(single.age === 25);
61
73
  }
74
+
75
+ @Test('Should not persist computed properties')
76
+ async testComputed() {
77
+ const service = await this.service;
78
+ const id = service.idSource.create();
79
+ await service.create(ComputedPerson, ComputedPerson.from({
80
+ id,
81
+ name: 'Bob',
82
+ ignoredField: 'secret'
83
+ }));
84
+
85
+ const retrieved = await service.get(ComputedPerson, id);
86
+ assert(retrieved.nameUpper === 'BOB');
87
+
88
+ // Verify it wasn't saved in the database holistically:
89
+ // When we fetch the document, the database driver retrieves a raw object and maps it.
90
+ // If the database stored 'nameUpper', trying to map it would set it on the retrieved instance.
91
+ // Since nameUpper is a getter-only property on the instance, we can verify that the persistence
92
+ // preparation (prePersist) recursively stripped the getter property from the stored object.
93
+ const instance = ComputedPerson.from({ id, name: 'Bob', ignoredField: 'secret' });
94
+ assert(Object.hasOwn(instance, 'nameUpper'));
95
+ assert(instance.ignoredField === 'secret');
96
+
97
+ const prepared = await ModelCrudUtil.prePersist(ComputedPerson, instance, 'all');
98
+ assert(prepared.nameUpper === undefined);
99
+ assert(prepared.ignoredField === undefined);
100
+ }
62
101
  }
@@ -92,7 +92,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
92
92
  const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
93
93
  assert(subContent.length === (range.end - range.start) + 1);
94
94
 
95
- const og = await this.fixture.readText('/text.txt');
95
+ const og = await this.fixture.readUTF8('/text.txt');
96
96
 
97
97
  assert(subContent === og.substring(10, 21));
98
98
 
@@ -3,7 +3,7 @@ import timers from 'node:timers/promises';
3
3
 
4
4
  import { Suite, Test } from '@travetto/test';
5
5
  import { Schema, Text, Precision, Required, } from '@travetto/schema';
6
- import { type ModelCrudSupport, Model, NotFoundError, PersistValue } from '@travetto/model';
6
+ import { type ModelCrudSupport, Model, NotFoundError, PersistValue, PrePersist } from '@travetto/model';
7
7
 
8
8
  import { BaseModelSuite } from './base.ts';
9
9
 
@@ -42,14 +42,13 @@ class SimpleList {
42
42
  }
43
43
 
44
44
  @Model()
45
+ @PrePersist((item) => {
46
+ item.name = `${item.name}-suffix`;
47
+ })
45
48
  class User2 {
46
49
  id: string;
47
50
  address?: Address;
48
51
  name: string;
49
-
50
- prePersist() {
51
- this.name = `${this.name}-suffix`;
52
- }
53
52
  }
54
53
 
55
54
  @Model()
@@ -77,6 +76,8 @@ class BigIntModel {
77
76
  @Suite()
78
77
  export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
79
78
 
79
+ indexLimitSkew = 0;
80
+
80
81
  @Test('save it')
81
82
  async save() {
82
83
  const service = await this.service;
@@ -268,6 +269,39 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
268
269
  assert(found[2].age === people[2].age);
269
270
  }
270
271
 
272
+ @Test('verify list abort signal')
273
+ async listAbortSignal() {
274
+ const service = await this.service;
275
+
276
+ await Promise.all(
277
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => service.upsert(Person, Person.from({
278
+ id: service.idSource.create(),
279
+ name: 'Bob',
280
+ age: 20 + x,
281
+ gender: 'm',
282
+ address: {
283
+ street1: 'a',
284
+ ...(x === 1 ? { street2: 'b' } : {})
285
+ }
286
+ })))
287
+ );
288
+
289
+ const controller = new AbortController();
290
+ const found: Person[] = [];
291
+
292
+ for await (const items of service.list(Person, { abort: controller.signal, batchSizeHint: 1 })) {
293
+ found.push(...items);
294
+ controller.abort();
295
+ await timers.setTimeout(10);
296
+ }
297
+
298
+ if (this.indexLimitSkew) {
299
+ assert(found.length > 0 && found.length < this.indexLimitSkew);
300
+ } else {
301
+ assert(found.length === 1);
302
+ }
303
+ }
304
+
271
305
  @Test('save it')
272
306
  async verifyRaw() {
273
307
  const service = await this.service;
@@ -3,13 +3,12 @@ import timers from 'node:timers/promises';
3
3
 
4
4
  import { Suite, Test } from '@travetto/test';
5
5
  import { castTo } from '@travetto/runtime';
6
- import { Schema, DiscriminatorField, Text, TypeMismatchError, Discriminated } from '@travetto/schema';
6
+ import { Schema, DiscriminatorField, Text, TypeMismatchError } from '@travetto/schema';
7
7
  import {
8
- type ModelIndexedSupport, Index, type ModelCrudSupport, Model,
8
+ type ModelCrudSupport, Model,
9
9
  NotFoundError, SubTypeNotSupportedError, PersistValue
10
10
  } from '@travetto/model';
11
11
 
12
- import { ModelIndexedUtil } from '../../src/util/indexed.ts';
13
12
  import { ExistsError } from '../../src/error/exists.ts';
14
13
 
15
14
  import { BaseModelSuite } from './base.ts';
@@ -43,42 +42,6 @@ export class Engineer extends Worker {
43
42
  major: string;
44
43
  }
45
44
 
46
- @Model()
47
- @Index({
48
- name: 'worker-name',
49
- type: 'sorted',
50
- fields: [{ name: 1 }, { age: 1 }]
51
- })
52
- @Discriminated('type')
53
- export class IndexedWorker {
54
- id: string;
55
- type: string;
56
- name: string;
57
- age?: number;
58
- }
59
- @Model()
60
- export class IndexedDoctor extends IndexedWorker {
61
- specialty: string;
62
- }
63
-
64
- @Model()
65
- export class IndexedFirefighter extends IndexedWorker {
66
- firehouse: number;
67
- }
68
-
69
- @Model()
70
- export class IndexedEngineer extends IndexedWorker {
71
- major: string;
72
- }
73
-
74
- async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
75
- const out: T[] = [];
76
- for await (const el of iterable) {
77
- out.push(el);
78
- }
79
- return out;
80
- }
81
-
82
45
  @Suite()
83
46
  export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSupport> {
84
47
 
@@ -108,7 +71,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
108
71
  const fire2 = await service.get(Worker, fire.id);
109
72
  assert(fire2 instanceof Firefighter);
110
73
 
111
- const all = await collect(service.list(Worker));
74
+ const all = await this.toArray(service.list(Worker));
112
75
  assert(all.length === 3);
113
76
 
114
77
  const doc3 = all.find(x => x instanceof Doctor);
@@ -126,7 +89,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
126
89
  assert(eng3.major === 'oranges');
127
90
  assert(eng3.name === 'cob');
128
91
 
129
- const engineers = await collect(service.list(Engineer));
92
+ const engineers = await this.toArray(service.list(Engineer));
130
93
  assert(engineers.length === 1);
131
94
 
132
95
  await service.create(Engineer, Engineer.from({
@@ -134,10 +97,10 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
134
97
  name: 'bob2'
135
98
  }));
136
99
 
137
- const all2 = await collect(service.list(Worker));
100
+ const all2 = await this.toArray(service.list(Worker));
138
101
  assert(all2.length === 4);
139
102
 
140
- const engineers2 = await collect(service.list(Engineer));
103
+ const engineers2 = await this.toArray(service.list(Engineer));
141
104
  assert(engineers2.length === 2);
142
105
  }
143
106
 
@@ -207,75 +170,4 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
207
170
  e => e instanceof SubTypeNotSupportedError || e instanceof NotFoundError
208
171
  );
209
172
  }
210
-
211
- @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
212
- async polymorphicIndexGet() {
213
- const service: ModelIndexedSupport = castTo(await this.service);
214
- const now = 30;
215
- const [doc, fire, eng] = [
216
- IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
217
- IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
218
- IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
219
- ];
220
-
221
- await this.saveAll(IndexedWorker, [doc, fire, eng]);
222
-
223
- const result = await service.getByIndex(IndexedWorker, 'worker-name', {
224
- age: now,
225
- name: 'rob'
226
- });
227
-
228
- assert(result instanceof IndexedFirefighter);
229
-
230
- try {
231
- const res2 = await service.getByIndex(IndexedFirefighter, 'worker-name', {
232
- age: now,
233
- name: 'rob'
234
- });
235
- assert(res2 instanceof IndexedFirefighter); // If service allows for get by subtype
236
- } catch (err) {
237
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError); // If it does not
238
- }
239
- }
240
-
241
- @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
242
- async polymorphicIndexDelete() {
243
- const service: ModelIndexedSupport = castTo(await this.service);
244
- const now = 30;
245
- const [doc, fire, eng] = [
246
- IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
247
- IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
248
- IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
249
- ];
250
-
251
- await this.saveAll(IndexedWorker, [doc, fire, eng]);
252
-
253
- assert(await this.getSize(IndexedWorker) === 3);
254
-
255
- await service.deleteByIndex(IndexedWorker, 'worker-name', {
256
- age: now,
257
- name: 'bob'
258
- });
259
-
260
- assert(await this.getSize(IndexedWorker) === 2);
261
- assert(await this.getSize(IndexedDoctor) === 0);
262
-
263
- try {
264
- await service.deleteByIndex(IndexedFirefighter, 'worker-name', {
265
- age: now,
266
- name: 'rob'
267
- });
268
- } catch (err) {
269
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
270
- }
271
-
272
- try {
273
- await service.deleteByIndex(IndexedEngineer, 'worker-name', {
274
- age: now,
275
- name: 'bob'
276
- });
277
- } catch (err) {
278
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
279
- }
280
- }
281
173
  }
@@ -1,43 +0,0 @@
1
- import type { Class, DeepPartial } from '@travetto/runtime';
2
-
3
- import type { ModelType, OptionalId } from '../types/model.ts';
4
- import type { ModelBasicSupport } from './basic.ts';
5
-
6
- /**
7
- * Support for simple indexed activity
8
- *
9
- * @concrete
10
- */
11
- export interface ModelIndexedSupport extends ModelBasicSupport {
12
- /**
13
- * Get entity by index as defined by fields of idx and the body fields
14
- * @param cls The type to search by
15
- * @param idx The index name to search against
16
- * @param body The payload of fields needed to search
17
- */
18
- getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
19
-
20
- /**
21
- * Delete entity by index as defined by fields of idx and the body fields
22
- * @param cls The type to search by
23
- * @param idx The index name to search against
24
- * @param body The payload of fields needed to search
25
- */
26
- deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
27
-
28
- /**
29
- * List entity by ranged index as defined by fields of idx and the body fields
30
- * @param cls The type to search by
31
- * @param idx The index name to search against
32
- * @param body The payload of fields needed to search
33
- */
34
- listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
35
-
36
- /**
37
- * Upsert by index, allowing the index to act as a primary key
38
- * @param cls The type to create for
39
- * @param idx The index name to use
40
- * @param body The document to potentially store
41
- */
42
- upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
43
- }
@@ -1,144 +0,0 @@
1
- import { castTo, type Class, type DeepPartial, hasFunction, TypedObject } from '@travetto/runtime';
2
-
3
- import { IndexNotSupported } from '../error/invalid-index.ts';
4
- import { NotFoundError } from '../error/not-found.ts';
5
- import type { IndexConfig } from '../registry/types.ts';
6
- import type { ModelCrudSupport } from '../types/crud.ts';
7
- import type { ModelIndexedSupport } from '../types/indexed.ts';
8
- import type { ModelType, OptionalId } from '../types/model.ts';
9
- import { ModelRegistryIndex } from '../registry/registry-index.ts';
10
-
11
- type ComputeConfig = {
12
- includeSortInFields?: boolean;
13
- emptyValue?: unknown;
14
- emptySortValue?: unknown;
15
- };
16
-
17
- type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
18
- type IndexSortPart = { path: string[], dir: number, value: number | Date };
19
-
20
- const DEFAULT_SEP = '\u8203';
21
-
22
- /**
23
- * Utils for working with indexed model services
24
- */
25
- export class ModelIndexedUtil {
26
-
27
- /**
28
- * Type guard for determining if service supports indexed operation
29
- */
30
- static isSupported = hasFunction<ModelIndexedSupport>('getByIndex');
31
-
32
- /**
33
- * Compute flattened field to value mappings
34
- * @param cls Class to get info for
35
- * @param idx Index config
36
- * @param item Item to read values from
37
- */
38
- static computeIndexParts<T extends ModelType>(
39
- cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
40
- ): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
41
- const config = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
42
- const sortField = config.type === 'sorted' ? config.fields.at(-1) : undefined;
43
-
44
- const fields: IndexFieldPart[] = [];
45
- let sortDirection: number = 0;
46
- let sorted: IndexSortPart | undefined;
47
-
48
- for (const field of config.fields) {
49
- let fieldRef: Record<string, unknown> = field;
50
- let itemRef: Record<string, unknown> = item;
51
- const parts = [];
52
-
53
- while (itemRef !== undefined && itemRef !== null) {
54
- const key = TypedObject.keys(fieldRef)[0];
55
- itemRef = castTo(itemRef[key]);
56
- parts.push(key);
57
- if (typeof fieldRef[key] === 'boolean' || typeof fieldRef[key] === 'number') {
58
- if (config.type === 'sorted') {
59
- sortDirection = fieldRef[key] === true ? 1 : fieldRef[key] === false ? 0 : fieldRef[key];
60
- }
61
- break; // At the bottom
62
- } else {
63
- fieldRef = castTo(fieldRef[key]);
64
- }
65
- }
66
- if (field === sortField) {
67
- sorted = { path: parts, dir: sortDirection, value: castTo(itemRef) };
68
- }
69
- if (itemRef === undefined || itemRef === null) {
70
- const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
71
- if (empty === undefined || empty === Error) {
72
- throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
73
- }
74
- } else {
75
- if (field !== sortField || (opts.includeSortInFields ?? true)) {
76
- fields.push({ path: parts, value: castTo(itemRef) });
77
- }
78
- }
79
- }
80
-
81
- return { fields, sorted };
82
- }
83
-
84
- /**
85
- * Project item via index
86
- * @param cls Type to get index for
87
- * @param idx Index config
88
- */
89
- static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, config?: ComputeConfig): Record<string, unknown> {
90
- const response: Record<string, unknown> = {};
91
- for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, config).fields) {
92
- let sub: Record<string, unknown> = response;
93
- const all = path.slice(0);
94
- const last = all.pop()!;
95
- for (const part of all) {
96
- sub = castTo(sub[part] ??= {});
97
- }
98
- sub[last] = value;
99
- }
100
- return response;
101
- }
102
-
103
- /**
104
- * Compute index key as a single value
105
- * @param cls Class to get index for
106
- * @param idx Index config
107
- * @param item item to process
108
- */
109
- static computeIndexKey<T extends ModelType>(
110
- cls: Class<T>,
111
- idx: IndexConfig<T> | string,
112
- item: DeepPartial<T> = {},
113
- config?: ComputeConfig & { separator?: string }
114
- ): { type: string, key: string, sort?: number | Date } {
115
- const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(config ?? {}), includeSortInFields: false });
116
- const key = fields.map(({ value }) => value).map(value => `${value}`).join(config?.separator ?? DEFAULT_SEP);
117
- const indexConfig = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
118
- return !sorted ? { type: indexConfig.type, key } : { type: indexConfig.type, key, sort: sorted.value };
119
- }
120
-
121
- /**
122
- * Naive upsert by index
123
- * @param service
124
- * @param cls
125
- * @param idx
126
- * @param body
127
- */
128
- static async naiveUpsert<T extends ModelType>(
129
- service: ModelIndexedSupport & ModelCrudSupport,
130
- cls: Class<T>, idx: string, body: OptionalId<T>
131
- ): Promise<T> {
132
- try {
133
- const { id } = await service.getByIndex(cls, idx, castTo(body));
134
- body.id = id;
135
- return await service.update(cls, castTo(body));
136
- } catch (error) {
137
- if (error instanceof NotFoundError) {
138
- return await service.create(cls, body);
139
- } else {
140
- throw error;
141
- }
142
- }
143
- }
144
- }