@travetto/model 8.0.0-alpha.9 → 8.0.0

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.
Files changed (42) hide show
  1. package/README.md +91 -102
  2. package/__index__.ts +7 -11
  3. package/package.json +18 -18
  4. package/src/error/exists.ts +1 -1
  5. package/src/error/invalid-index.ts +5 -3
  6. package/src/error/invalid-sub-type.ts +1 -1
  7. package/src/error/not-found.ts +1 -1
  8. package/src/error/unique.ts +13 -0
  9. package/src/registry/decorator.ts +38 -32
  10. package/src/registry/registry-adapter.ts +11 -8
  11. package/src/registry/registry-index.ts +12 -33
  12. package/src/registry/types.ts +24 -39
  13. package/src/types/basic.ts +3 -3
  14. package/src/types/blob.ts +6 -6
  15. package/src/types/bulk.ts +5 -9
  16. package/src/types/crud.ts +9 -6
  17. package/src/types/expiry.ts +2 -2
  18. package/src/types/model.ts +11 -2
  19. package/src/types/storage.ts +2 -3
  20. package/src/util/blob.ts +2 -2
  21. package/src/util/bulk.ts +6 -3
  22. package/src/util/crud.ts +105 -22
  23. package/src/util/expiry.ts +4 -5
  24. package/src/util/storage.ts +3 -4
  25. package/support/base-command.ts +3 -5
  26. package/support/bin/candidate.ts +8 -11
  27. package/support/bin/export.ts +2 -2
  28. package/support/bin/install.ts +2 -2
  29. package/support/cli.model_export.ts +9 -5
  30. package/support/cli.model_install.ts +9 -5
  31. package/support/doc.support.tsx +16 -20
  32. package/support/test/base.ts +14 -12
  33. package/support/test/basic.ts +44 -3
  34. package/support/test/blob.ts +8 -9
  35. package/support/test/bulk.ts +16 -5
  36. package/support/test/crud.ts +134 -82
  37. package/support/test/expiry.ts +39 -21
  38. package/support/test/polymorphism.ts +43 -143
  39. package/support/test/suite.ts +15 -12
  40. package/src/types/indexed.ts +0 -43
  41. package/src/util/indexed.ts +0 -144
  42. package/support/test/indexed.ts +0 -190
@@ -7,9 +7,10 @@ import type { ModelConfig } from './types.ts';
7
7
  function combineClasses(target: ModelConfig, sources: Partial<ModelConfig>[]): ModelConfig {
8
8
  for (const source of sources) {
9
9
  Object.assign(target, source, {
10
- indices: [...(target.indices || []), ...(source.indices || [])],
10
+ indices: { ...(target.indices || {}), ...(source.indices || {}) },
11
11
  postLoad: [...(target.postLoad || []), ...(source.postLoad || [])],
12
12
  prePersist: [...(target.prePersist || []), ...(source.prePersist || [])],
13
+ transientFields: [...(target.transientFields || []), ...(source.transientFields || [])]
13
14
  });
14
15
  }
15
16
  if (target.store) {
@@ -27,14 +28,15 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
27
28
  }
28
29
 
29
30
  register(...data: Partial<ModelConfig>[]): ModelConfig {
30
- const config = this.#config ??= {
31
+ const config = (this.#config ??= {
31
32
  class: this.#cls,
32
- indices: [],
33
+ indices: {},
33
34
  autoCreate: 'development',
34
35
  store: this.#cls.name,
35
36
  postLoad: [],
36
- prePersist: []
37
- };
37
+ prePersist: [],
38
+ transientFields: []
39
+ });
38
40
  combineClasses(config, data);
39
41
  return config;
40
42
  }
@@ -48,12 +50,13 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
48
50
  config.store = parent.store;
49
51
  }
50
52
 
51
- config.postLoad = [...parent.postLoad ?? [], ...config.postLoad ?? []];
52
- config.prePersist = [...parent.prePersist ?? [], ...config.prePersist ?? []];
53
+ config.postLoad = [...(parent.postLoad ?? []), ...(config.postLoad ?? [])];
54
+ config.prePersist = [...(parent.prePersist ?? []), ...(config.prePersist ?? [])];
55
+ config.transientFields = [...(parent.transientFields ?? []), ...(config.transientFields ?? [])];
53
56
  }
54
57
  }
55
58
 
56
59
  get(): ModelConfig {
57
60
  return this.#config;
58
61
  }
59
- }
62
+ }
@@ -1,20 +1,15 @@
1
- import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
- import { RuntimeError, castTo, type Class } from '@travetto/runtime';
1
+ import { Registry, type RegistryIndex, RegistryIndexStore } from '@travetto/registry';
2
+ import { type Class, castTo, RuntimeError } from '@travetto/runtime';
3
3
  import { SchemaRegistryIndex } from '@travetto/schema';
4
4
 
5
- import type { IndexConfig, IndexType, ModelConfig } from './types.ts';
6
5
  import type { ModelType } from '../types/model.ts';
7
6
  import { ModelRegistryAdapter } from './registry-adapter.ts';
8
- import { IndexNotSupported } from '../error/invalid-index.ts';
9
- import { NotFoundError } from '../error/not-found.ts';
10
-
11
- type IndexResult<T extends ModelType, K extends IndexType[]> = IndexConfig<T> & { type: K[number] };
7
+ import type { IndexConfig, ModelConfig } from './types.ts';
12
8
 
13
9
  /**
14
10
  * Model registry index for managing model configurations across classes
15
11
  */
16
12
  export class ModelRegistryIndex implements RegistryIndex {
17
-
18
13
  static #instance = Registry.registerIndex(this);
19
14
 
20
15
  static getForRegister(cls: Class): ModelRegistryAdapter {
@@ -33,12 +28,8 @@ export class ModelRegistryIndex implements RegistryIndex {
33
28
  return this.#instance.getStoreName(cls);
34
29
  }
35
30
 
36
- static getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
37
- return this.#instance.getIndices(cls, supportedTypes);
38
- }
39
-
40
- static getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
41
- return this.#instance.getIndex(cls, name, supportedTypes);
31
+ static getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
32
+ return this.#instance.getIndices(cls);
42
33
  }
43
34
 
44
35
  static getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
@@ -58,7 +49,9 @@ export class ModelRegistryIndex implements RegistryIndex {
58
49
 
59
50
  store = new RegistryIndexStore(ModelRegistryAdapter);
60
51
 
61
- /** @private */ constructor(source: unknown) { Registry.validateConstructor(source); }
52
+ /** @private */ constructor(source: unknown) {
53
+ Registry.validateConstructor(source);
54
+ }
62
55
 
63
56
  onCreate(cls: Class): void {
64
57
  const schema = SchemaRegistryIndex.getConfig(cls);
@@ -71,7 +64,7 @@ export class ModelRegistryIndex implements RegistryIndex {
71
64
  const { store } = this.getConfig(cls);
72
65
  let classes = this.#modelNameMapping.get(store);
73
66
  if (!classes) {
74
- this.#modelNameMapping.set(store, classes = new Set());
67
+ this.#modelNameMapping.set(store, (classes = new Set()));
75
68
  }
76
69
  classes.add(cls.Ⲑid);
77
70
 
@@ -94,25 +87,11 @@ export class ModelRegistryIndex implements RegistryIndex {
94
87
  return this.store.get(cls).get().store;
95
88
  }
96
89
 
97
- /**
98
- * Get Index
99
- */
100
- getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
101
- const config = this.getConfig(cls).indices?.find((idx): idx is IndexConfig<T> => idx.name === name);
102
- if (!config) {
103
- throw new NotFoundError(`${cls.name} Index`, `${name}`);
104
- }
105
- if (supportedTypes && !supportedTypes.includes(config.type)) {
106
- throw new IndexNotSupported(cls, config, `${config.type} indices are not supported.`);
107
- }
108
- return config;
109
- }
110
-
111
90
  /**
112
91
  * Get Indices
113
92
  */
114
- getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
115
- return (this.getConfig(cls).indices ?? []).filter((idx): idx is IndexConfig<T> => !supportedTypes || supportedTypes.includes(idx.type));
93
+ getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
94
+ return Object.values(this.getConfig(cls).indices ?? []);
116
95
  }
117
96
 
118
97
  /**
@@ -126,4 +105,4 @@ export class ModelRegistryIndex implements RegistryIndex {
126
105
  }
127
106
  return castTo(expiry);
128
107
  }
129
- }
108
+ }
@@ -1,23 +1,29 @@
1
- import type { Class, Primitive, ValidFields } from '@travetto/runtime';
1
+ import type { Class } from '@travetto/runtime';
2
2
 
3
3
  import type { ModelType } from '../types/model.ts';
4
4
 
5
- type RetainPrimitiveFields<T> = Pick<T, ValidFields<T, Primitive | Date>>;
5
+ export type DataHandler<T = unknown> = (inst: T) => Promise<T | void> | T | void;
6
6
 
7
- export type SortClauseRaw<T> = {
8
- [P in keyof T]?:
9
- T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
10
- };
7
+ export type PrePersistScope = 'full' | 'partial' | 'all';
11
8
 
12
- type IndexClauseRaw<T> = {
13
- [P in keyof T]?:
14
- T[P] extends object ? IndexClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1 | true;
9
+ /**
10
+ * Index options
11
+ */
12
+ export type IndexConfig<V extends string = string, T extends ModelType = ModelType> = {
13
+ /**
14
+ * Index name
15
+ */
16
+ name: string;
17
+ /**
18
+ * Type
19
+ */
20
+ type: V;
21
+ /**
22
+ * Class the index belongs to
23
+ */
24
+ class: Class<T>;
15
25
  };
16
26
 
17
- export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
18
-
19
- export type PrePersistScope = 'full' | 'partial' | 'all';
20
-
21
27
  /**
22
28
  * Model config
23
29
  */
@@ -33,7 +39,7 @@ export class ModelConfig<T extends ModelType = ModelType> {
33
39
  /**
34
40
  * Indices
35
41
  */
36
- indices?: IndexConfig<T>[];
42
+ indices?: Record<string, IndexConfig>;
37
43
  /**
38
44
  * Vendor specific extras
39
45
  */
@@ -49,34 +55,13 @@ export class ModelConfig<T extends ModelType = ModelType> {
49
55
  /**
50
56
  * Pre-persist handlers
51
57
  */
52
- prePersist?: { scope: PrePersistScope, handler: DataHandler<unknown> }[];
58
+ prePersist?: { scope: PrePersistScope; handler: DataHandler<unknown> }[];
53
59
  /**
54
60
  * Post-load handlers
55
61
  */
56
62
  postLoad?: DataHandler<unknown>[];
57
- }
58
-
59
- /**
60
- * Supported index types
61
- */
62
- export type IndexType = 'unique' | 'unsorted' | 'sorted';
63
-
64
- /**
65
- * Index options
66
- */
67
- export type IndexConfig<T extends ModelType> = {
68
- /**
69
- * Index name
70
- */
71
- name: string;
72
63
  /**
73
- * Fields and sort order
64
+ * Fields to ignore during persistence
74
65
  */
75
- fields: IndexClauseRaw<RetainPrimitiveFields<T>>[];
76
- /**
77
- * Type
78
- */
79
- type: IndexType;
80
- };
81
-
82
- export type IndexField<T extends ModelType> = IndexClauseRaw<RetainPrimitiveFields<T>>;
66
+ transientFields?: string[];
67
+ }
@@ -1,12 +1,12 @@
1
1
  import type { Class } from '@travetto/runtime';
2
- import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
2
+
3
+ import type { ModelIdSource, ModelType, OptionalId } from './model.ts';
3
4
 
4
5
  /**
5
6
  * Interface for basic data interface
6
7
  * @concrete
7
8
  */
8
9
  export interface ModelBasicSupport<C = unknown> {
9
-
10
10
  /**
11
11
  * Id Source
12
12
  */
@@ -37,4 +37,4 @@ export interface ModelBasicSupport<C = unknown> {
37
37
  * @throws {NotFoundError} When an item is not found
38
38
  */
39
39
  delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void>;
40
- }
40
+ }
package/src/types/blob.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BinaryType, BinaryMetadata, ByteRange, TimeSpan } from '@travetto/runtime';
1
+ import type { BinaryMetadata, BinaryType, ByteRange, TimeSpan } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Support for Blobs CRUD.
@@ -6,7 +6,6 @@ import type { BinaryType, BinaryMetadata, ByteRange, TimeSpan } from '@travetto/
6
6
  * @concrete
7
7
  */
8
8
  export interface ModelBlobSupport {
9
-
10
9
  /**
11
10
  * Upsert blob to storage
12
11
  * @param location The location of the blob
@@ -42,12 +41,13 @@ export interface ModelBlobSupport {
42
41
  updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
43
42
 
44
43
  /**
45
- * Produces an externally usable URL for sharing limited read access to a specific resource
44
+ * Produces an externally usable URL for sharing limited read access to a specific resource.
45
+ * If expiresIn is explicitly set to false, returns a direct/public URL.
46
46
  *
47
47
  * @param location The asset location to read from
48
- * @param expiresIn Expiry
48
+ * @param expiresIn Expiry or false for public/direct URL
49
49
  */
50
- getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
50
+ getBlobReadUrl?(location: string, expiresIn?: TimeSpan | false): Promise<string>;
51
51
 
52
52
  /**
53
53
  * Produces an externally usable URL for sharing allowing direct write access
@@ -57,4 +57,4 @@ export interface ModelBlobSupport {
57
57
  * @param expiresIn Expiry
58
58
  */
59
59
  getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
60
- }
60
+ }
package/src/types/bulk.ts CHANGED
@@ -2,16 +2,12 @@ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
  import type { ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import type { ModelCrudSupport } from './crud.ts';
5
- import type { ModelType, OptionalId } from '../types/model.ts';
5
+ import type { ModelType, OptionalId } from './model.ts';
6
6
 
7
7
  /**
8
8
  * Bulk operation. Each operation has a single action and payload
9
9
  */
10
- export type BulkOperation<T extends ModelType> =
11
- { delete?: T } &
12
- { insert?: OptionalId<T> } &
13
- { update?: T } &
14
- { upsert?: OptionalId<T> };
10
+ export type BulkOperation<T extends ModelType> = { delete?: T } & { insert?: OptionalId<T> } & { update?: T } & { upsert?: OptionalId<T> };
15
11
 
16
12
  /**
17
13
  * Bulk response provides a summary of all the operations
@@ -37,13 +33,13 @@ export interface BulkResponse<E = unknown> {
37
33
  };
38
34
  }
39
35
 
40
- type BulkErrorItem = { message: string, type: string, errors?: ValidationError[], idx: number };
36
+ type BulkErrorItem = { message: string; type: string; errors?: ValidationError[]; idx: number };
41
37
 
42
38
  /**
43
39
  * Bulk processing error
44
40
  */
45
41
  export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }> {
46
- constructor(errors: { idx: number, error: ValidationResultError }[]) {
42
+ constructor(errors: { idx: number; error: ValidationResultError }[]) {
47
43
  super('Bulk processing errors have occurred', {
48
44
  category: 'data',
49
45
  details: {
@@ -63,4 +59,4 @@ export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }>
63
59
  */
64
60
  export interface ModelBulkSupport extends ModelCrudSupport {
65
61
  processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse>;
66
- }
62
+ }
package/src/types/crud.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import type { ModelType, OptionalId } from '../types/model.ts';
4
-
5
3
  import type { ModelBasicSupport } from './basic.ts';
4
+ import type { ModelListOptions, ModelType, OptionalId } from './model.ts';
6
5
 
7
6
  /**
8
7
  * Interface for simple CRUD
9
8
  * @concrete
10
9
  */
11
10
  export interface ModelCrudSupport extends ModelBasicSupport {
12
-
13
11
  /**
14
12
  * Update an item
15
13
  * @param item The document to update.
@@ -38,7 +36,12 @@ export interface ModelCrudSupport extends ModelBasicSupport {
38
36
  updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T>;
39
37
 
40
38
  /**
41
- * List all items
39
+ * List all items of a collection, results returned in batches of items.
40
+ *
41
+ * Note: Batch size hint can be used to optimize batch size, but is not guaranteed.
42
+ *
43
+ * @param cls The class to list
44
+ * @param options Options for listing
42
45
  */
43
- list<T extends ModelType>(cls: Class<T>): AsyncIterable<T>;
44
- }
46
+ list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
47
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import type { ModelType } from '../types/model.ts';
4
3
  import type { ModelCrudSupport } from './crud.ts';
4
+ import type { ModelType } from './model.ts';
5
5
 
6
6
  /**
7
7
  * Support for managing expiration of data
@@ -14,4 +14,4 @@ export interface ModelExpirySupport extends ModelCrudSupport {
14
14
  * @returns Returns the number of documents expired
15
15
  */
16
16
  deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number>;
17
- }
17
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Model Id Source
3
3
  */
4
- export type ModelIdSource = { create: () => string, valid: (id: string) => boolean };
4
+ export type ModelIdSource = { create: () => string; valid: (id: string) => boolean };
5
5
 
6
6
  /**
7
7
  * Simple model interface
@@ -16,4 +16,13 @@ export interface ModelType {
16
16
  id: string;
17
17
  }
18
18
 
19
- export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
19
+ export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
20
+
21
+ /**
22
+ * Options for listing items
23
+ */
24
+ export interface ModelListOptions {
25
+ abort?: AbortSignal;
26
+ limit?: number;
27
+ batchSizeHint?: number;
28
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import type { ModelType } from '../types/model.ts';
3
+ import type { ModelType } from './model.ts';
4
4
 
5
5
  /**
6
6
  * This interface defines the behavior for dealing with the
@@ -12,7 +12,6 @@ import type { ModelType } from '../types/model.ts';
12
12
  * @concrete
13
13
  */
14
14
  export interface ModelStorageSupport {
15
-
16
15
  /**
17
16
  * Should storage modification be allowed
18
17
  */
@@ -48,4 +47,4 @@ export interface ModelStorageSupport {
48
47
  * Truncate blob storage data
49
48
  */
50
49
  truncateBlob?(): Promise<void>;
51
- }
50
+ }
package/src/util/blob.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { hasFunction } from '@travetto/runtime';
2
+
2
3
  import type { ModelBlobSupport } from '../types/blob.ts';
3
4
 
4
5
  /**
5
6
  * Utilities for processing blobs
6
7
  */
7
8
  export class ModelBlobUtil {
8
-
9
9
  /**
10
10
  * Type guard for determining if service supports blob operations
11
11
  */
@@ -15,4 +15,4 @@ export class ModelBlobUtil {
15
15
  * Type guard for determining if service supports blob write urls
16
16
  */
17
17
  static isWriteUrlSupported = hasFunction<ModelBlobSupport>('getBlobWriteUrl');
18
- }
18
+ }
package/src/util/bulk.ts CHANGED
@@ -13,7 +13,6 @@ export type BulkPreStore<T extends ModelType> = {
13
13
  };
14
14
 
15
15
  export class ModelBulkUtil {
16
-
17
16
  /**
18
17
  * Type guard for determining if service supports bulk operation
19
18
  */
@@ -25,7 +24,11 @@ export class ModelBulkUtil {
25
24
  * @param operations
26
25
  * @param provider
27
26
  */
28
- static async preStore<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[], provider: ModelCrudProvider): Promise<BulkPreStore<T>> {
27
+ static async preStore<T extends ModelType>(
28
+ cls: Class<T>,
29
+ operations: BulkOperation<T>[],
30
+ provider: ModelCrudProvider
31
+ ): Promise<BulkPreStore<T>> {
29
32
  const insertedIds = new Map<number, string>();
30
33
  const upsertedIds = new Map<number, string>();
31
34
  const updatedIds = new Map<number, string>();
@@ -53,4 +56,4 @@ export class ModelBulkUtil {
53
56
  }
54
57
  return { insertedIds, upsertedIds, updatedIds, existingUpsertedIds, operations };
55
58
  }
56
- }
59
+ }
package/src/util/crud.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { castTo, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
1
+ import { type BinaryArray, BinaryUtil, type Class, castKey, castTo, hasFunction, JSONUtil, RuntimeError, Util } from '@travetto/runtime';
2
2
  import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
- import { ModelRegistryIndex } from '../registry/registry-index.ts';
5
- import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
6
- import { NotFoundError } from '../error/not-found.ts';
7
4
  import { ExistsError } from '../error/exists.ts';
8
5
  import { SubTypeNotSupportedError } from '../error/invalid-sub-type.ts';
6
+ import { NotFoundError } from '../error/not-found.ts';
7
+ import { ModelRegistryIndex } from '../registry/registry-index.ts';
9
8
  import type { DataHandler, PrePersistScope } from '../registry/types.ts';
10
9
  import type { ModelCrudSupport } from '../types/crud.ts';
10
+ import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
11
11
 
12
12
  type ModelLoadInput = string | BinaryArray | object;
13
13
 
@@ -19,7 +19,6 @@ export type ModelCrudProvider = {
19
19
  * Crud utilities
20
20
  */
21
21
  export class ModelCrudUtil {
22
-
23
22
  /**
24
23
  * Type guard for determining if service supports crud operations
25
24
  */
@@ -34,16 +33,35 @@ export class ModelCrudUtil {
34
33
  return { create, valid };
35
34
  }
36
35
 
36
+ static async filterOutNotFound<T extends ModelType>(actions: Promise<T>[] | undefined): Promise<T[]> {
37
+ if (!actions) {
38
+ return [];
39
+ }
40
+ return (await Promise.allSettled(actions))
41
+ .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
+ })
50
+ .filter(item => !!item);
51
+ }
52
+
37
53
  /**
38
54
  * Load model
39
55
  * @param cls Class to load model for
40
56
  * @param input Input as string or plain object
41
57
  */
42
- static async load<T extends ModelType>(cls: Class<T>, input: ModelLoadInput, onTypeMismatch: 'notfound' | 'exists' = 'notfound'): Promise<T> {
58
+ static async load<T extends ModelType>(
59
+ cls: Class<T>,
60
+ input: ModelLoadInput,
61
+ onTypeMismatch: 'notfound' | 'exists' = 'notfound'
62
+ ): Promise<T> {
43
63
  const resolvedInput: object =
44
- typeof input === 'string' ? JSONUtil.fromUTF8(input) :
45
- BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) :
46
- input;
64
+ typeof input === 'string' ? JSONUtil.fromUTF8(input) : BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) : input;
47
65
 
48
66
  const result = SchemaRegistryIndex.getBaseClass(cls).from(resolvedInput);
49
67
 
@@ -64,7 +82,12 @@ export class ModelCrudUtil {
64
82
  * @param cls Type to store for
65
83
  * @param item Item to store
66
84
  */
67
- static async preStore<T extends ModelType>(cls: Class<T>, item: Partial<OptionalId<T>>, provider: ModelCrudProvider, scope: PrePersistScope = 'all'): Promise<T> {
85
+ static async preStore<T extends ModelType>(
86
+ cls: Class<T>,
87
+ item: Partial<OptionalId<T>>,
88
+ provider: ModelCrudProvider,
89
+ scope: PrePersistScope = 'all'
90
+ ): Promise<T> {
68
91
  if (!item.id) {
69
92
  item.id = provider.idSource.create();
70
93
  }
@@ -96,6 +119,70 @@ export class ModelCrudUtil {
96
119
  return castTo(item);
97
120
  }
98
121
 
122
+ /**
123
+ * Recursively deletes or masks transient fields from an object based on its Model config.
124
+ * If transient fields are present, returns a copy with those fields set to undefined.
125
+ */
126
+ static cleanTransientFields<T>(cls: Class<T>, item: T): T {
127
+ if (!item || typeof item !== 'object') {
128
+ return item;
129
+ }
130
+ if (SchemaRegistryIndex.has(cls)) {
131
+ const schema = SchemaRegistryIndex.get(cls).get();
132
+ const transientFields = ModelRegistryIndex.has(cls) ? ModelRegistryIndex.getConfig(cls).transientFields : undefined;
133
+
134
+ const isCleanTarget = (key: string): boolean => {
135
+ return transientFields?.includes(key) ?? false;
136
+ };
137
+
138
+ const hasTargets = Object.keys(schema.fields).some(key => isCleanTarget(key));
139
+
140
+ let res = item;
141
+ if (hasTargets) {
142
+ res = { ...item };
143
+ for (const key of Object.keys(schema.fields)) {
144
+ if (isCleanTarget(key)) {
145
+ res[castKey<T>(key)] = undefined!;
146
+ }
147
+ }
148
+ }
149
+
150
+ for (const [key, field] of Object.entries(schema.fields)) {
151
+ const fieldKey = castKey<typeof res>(key);
152
+ if (!isCleanTarget(key) && res[fieldKey] !== undefined && res[fieldKey] !== null) {
153
+ if (field.array && Array.isArray(res[fieldKey])) {
154
+ const arr = res[fieldKey];
155
+ let changed = false;
156
+ const newArray = arr.map(subItem => {
157
+ const cleaned = this.cleanTransientFields(field.type, subItem);
158
+ if (cleaned !== subItem) {
159
+ changed = true;
160
+ }
161
+ return cleaned;
162
+ });
163
+ if (changed) {
164
+ if (res === item) {
165
+ res = { ...item };
166
+ }
167
+ res[fieldKey] = castTo(newArray);
168
+ }
169
+ } else {
170
+ const val = res[fieldKey];
171
+ const cleaned = this.cleanTransientFields(field.type, val);
172
+ if (cleaned !== val) {
173
+ if (res === item) {
174
+ res = { ...item };
175
+ }
176
+ res[fieldKey] = cleaned;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ return res;
182
+ }
183
+ return item;
184
+ }
185
+
99
186
  /**
100
187
  * Ensure subtype is not supported
101
188
  */
@@ -111,16 +198,13 @@ export class ModelCrudUtil {
111
198
  */
112
199
  static async prePersist<T>(cls: Class<T>, item: T, scope: PrePersistScope): Promise<T> {
113
200
  const config = ModelRegistryIndex.getConfig(cls);
114
- for (const state of (config.prePersist ?? [])) {
201
+ for (const state of config.prePersist ?? []) {
115
202
  if (state.scope === scope || scope === 'all' || state.scope === 'all') {
116
203
  const handler: DataHandler<T> = castTo(state.handler);
117
- item = await handler(item) ?? item;
204
+ item = (await handler(item)) ?? item;
118
205
  }
119
206
  }
120
- if (typeof item === 'object' && item && 'prePersist' in item && typeof item['prePersist'] === 'function') {
121
- item = await item.prePersist() ?? item;
122
- }
123
- return item;
207
+ return this.cleanTransientFields(cls, item);
124
208
  }
125
209
 
126
210
  /**
@@ -129,10 +213,7 @@ export class ModelCrudUtil {
129
213
  static async postLoad<T>(cls: Class<T>, item: T): Promise<T> {
130
214
  const config = ModelRegistryIndex.getConfig(cls);
131
215
  for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
132
- item = await handler(item) ?? item;
133
- }
134
- if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
135
- item = await item.postLoad() ?? item;
216
+ item = (await handler(item)) ?? item;
136
217
  }
137
218
  return item;
138
219
  }
@@ -142,7 +223,9 @@ export class ModelCrudUtil {
142
223
  */
143
224
  static async prePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view?: string): Promise<Partial<T>> {
144
225
  if (!DataUtil.isPlainObject(item)) {
145
- throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, { category: 'data' });
226
+ throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, {
227
+ category: 'data'
228
+ });
146
229
  }
147
230
  const keys = Object.keys(item);
148
231
  if ((keys.length === 1 && item.id) || keys.length === 0) {
@@ -164,4 +247,4 @@ export class ModelCrudUtil {
164
247
  const full = await get();
165
248
  return cls.from(castTo({ ...full, ...prepared }));
166
249
  }
167
- }
250
+ }