@happyvertical/smrt-core 0.38.18 → 0.38.20
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/AGENTS.md +28 -0
- package/dist/collection.d.ts +82 -41
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +92 -57
- package/dist/collection.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/interceptors.d.ts +2 -1
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/learning/index.d.ts +8 -0
- package/dist/learning/index.d.ts.map +1 -0
- package/dist/learning/memory.d.ts +226 -0
- package/dist/learning/memory.d.ts.map +1 -0
- package/dist/learning/memory.js +340 -0
- package/dist/learning/memory.js.map +1 -0
- package/dist/manifest/static-manifest.js +4 -4
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1027 -229
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +4 -4
- package/dist/runtime/types.d.ts +1 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/testing/database.d.ts +5 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +4 -1
- package/dist/testing/database.js.map +1 -1
- package/package.json +4 -4
package/AGENTS.md
CHANGED
|
@@ -11,6 +11,7 @@ ORM, code generation, AI integration, and the DispatchBus. Everything else build
|
|
|
11
11
|
| ObjectRegistry | `src/registry.ts` | Global singleton (globalThis) — class metadata, fields, STI chains, manifests |
|
|
12
12
|
| DispatchBus | `src/dispatch/bus.ts` | Inter-agent messaging — emit, subscribe (persistent), process |
|
|
13
13
|
| GlobalInterceptors | `src/interceptors.ts` | Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |
|
|
14
|
+
| LearningMemory | `src/learning/memory.ts` | Confidence-scored recall/capture over `_smrt_contexts` + embeddings (#1886) |
|
|
14
15
|
|
|
15
16
|
## SmrtObject Lifecycle
|
|
16
17
|
|
|
@@ -22,6 +23,25 @@ ORM, code generation, AI integration, and the DispatchBus. Everything else build
|
|
|
22
23
|
- `getSlug()`: auto-generates from name → title → label → id
|
|
23
24
|
- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)
|
|
24
25
|
|
|
26
|
+
## LearningMemory (#1886)
|
|
27
|
+
|
|
28
|
+
Confidence-scored, self-correcting memory over the existing `_smrt_contexts` (keyed recall) and `_smrt_embeddings` (semantic recall) substrate. Wires the reinforcement columns that ship on `_smrt_contexts` but were never written (`success_count`, `failure_count`, and a `last_used_at` that recall now refreshes). This is L1 of the tenant-learning-agents epic; the opt-in `Learning` trait in `@happyvertical/smrt-agents` composes it into the agent lifecycle.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const memory = new LearningMemory({ db: obj.systemDb, ownerClass: 'InvoiceAgent', ownerId: obj.id, tenantId });
|
|
32
|
+
|
|
33
|
+
// recall — union of keyed-context lookup + (optional) semantic search, confidence-filtered
|
|
34
|
+
const [hit] = await memory.recall('parser/acme', { key: docUrl }); // or { query } with a wired semanticSearch
|
|
35
|
+
const strategy = hit?.value ?? (await generate());
|
|
36
|
+
|
|
37
|
+
// capture — reinforce the outcome
|
|
38
|
+
await memory.capture({ scope: 'parser/acme', key: docUrl, value: strategy }, { success: ok });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **`capture(episode, outcome)`**: success strengthens `confidence` toward 1.0 + increments `success_count`; failure decays toward `failureConfidence` (default 0.3) + increments `failure_count`. Defaults (`minConfidence` 0.7, `successConfidence` 0.9, `reinforcement` 0.5) mean a single failure drops a confident memory below the reuse floor. Seeds a new row when none exists and the episode carries a `value` (a failed first attempt is retained at low confidence for self-correction).
|
|
42
|
+
- **`recall(scope, opts)`**: owner-scoped keyed lookup (thus tenant-isolated) filtered by the confidence floor, expiry, and optional time-decay, with hierarchical scope fallback; unions an injected `semanticSearch` arm when a `query` is given (tenant-scoped via its `where`). Refreshes `last_used_at` on returned rows.
|
|
43
|
+
- Injected `semanticSearch` matches `SmrtCollection.semanticSearch`, so `LearningMemory` never reaches into a collection's internals.
|
|
44
|
+
|
|
25
45
|
## SmrtCollection Query
|
|
26
46
|
|
|
27
47
|
```typescript
|
|
@@ -31,6 +51,14 @@ await collection.list({
|
|
|
31
51
|
});
|
|
32
52
|
```
|
|
33
53
|
|
|
54
|
+
Projection primitive (#1902): pass `select: ['id', 'title', 'tenantId']` to
|
|
55
|
+
`list()` when an admin/list workflow needs compact rows. `select` uses SMRT
|
|
56
|
+
field names, maps them to DB columns internally, and returns plain objects keyed
|
|
57
|
+
by the same SMRT field names without hydrating `SmrtObject` instances. It
|
|
58
|
+
composes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`
|
|
59
|
+
interceptors still run. It is for column-backed fields only and cannot combine
|
|
60
|
+
with `include`/relationship eager loading.
|
|
61
|
+
|
|
34
62
|
**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`, `is null`, `is not null`. Arrays auto-detect `IN`. Dot notation for JSON paths: `metadata.userId`.
|
|
35
63
|
|
|
36
64
|
STI child collections auto-filter by `_meta_type`.
|
package/dist/collection.d.ts
CHANGED
|
@@ -51,20 +51,72 @@ export type SmrtCreateInput<T extends SmrtObject> = Partial<Omit<{
|
|
|
51
51
|
* `convertWhereKeys()` handles field and operator checking.
|
|
52
52
|
*/
|
|
53
53
|
export type SmrtWhereClause<T extends SmrtObject> = Record<string, unknown>;
|
|
54
|
+
type SmrtModelData<T extends SmrtObject> = Omit<{
|
|
55
|
+
[K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]: T[K];
|
|
56
|
+
}, SmrtInternalKeys>;
|
|
57
|
+
/**
|
|
58
|
+
* Logical field names accepted by `collection.list({ select })`.
|
|
59
|
+
*
|
|
60
|
+
* `select` is intentionally a column-backed projection primitive: callers pass
|
|
61
|
+
* SMRT field names such as `parentId`, and the collection maps them to database
|
|
62
|
+
* columns such as `parent_id` while returning rows keyed by the original SMRT
|
|
63
|
+
* field names.
|
|
64
|
+
*/
|
|
65
|
+
export type SmrtSelectField<T extends SmrtObject> = Extract<keyof SmrtModelData<T>, string> | 'id' | 'slug' | 'context' | 'created_at' | 'updated_at' | '_meta_type';
|
|
66
|
+
type SmrtCoreSelectedFieldData = {
|
|
67
|
+
id: string | null | undefined;
|
|
68
|
+
slug: string | null | undefined;
|
|
69
|
+
context: string;
|
|
70
|
+
created_at: Date | null | undefined;
|
|
71
|
+
updated_at: Date | null | undefined;
|
|
72
|
+
_meta_type: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Plain row shape returned by `collection.list({ select })`.
|
|
76
|
+
*/
|
|
77
|
+
export type SmrtSelectedRow<T extends SmrtObject, Select extends readonly SmrtSelectField<T>[]> = {
|
|
78
|
+
[Field in Select[number]]: Field extends keyof SmrtCoreSelectedFieldData ? SmrtCoreSelectedFieldData[Field] : Field extends keyof SmrtModelData<T> ? SmrtModelData<T>[Field] : unknown;
|
|
79
|
+
};
|
|
80
|
+
export interface SmrtListOptions<ModelType extends SmrtObject> {
|
|
81
|
+
where?: SmrtWhereClause<ModelType>;
|
|
82
|
+
offset?: number;
|
|
83
|
+
limit?: number;
|
|
84
|
+
orderBy?: string | string[];
|
|
85
|
+
/**
|
|
86
|
+
* Column-backed field projection. When present, `list()` returns plain rows
|
|
87
|
+
* instead of hydrated model instances.
|
|
88
|
+
*/
|
|
89
|
+
select?: readonly SmrtSelectField<ModelType>[];
|
|
90
|
+
/**
|
|
91
|
+
* Relationships to eagerly load. Only supported for hydrated list() calls.
|
|
92
|
+
*/
|
|
93
|
+
include?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* Opt-in read-through cache for this call (issue #1498).
|
|
96
|
+
*/
|
|
97
|
+
cache?: CollectionCacheConfig | false;
|
|
98
|
+
}
|
|
54
99
|
/**
|
|
55
100
|
* Minimal runtime shape of a field definition as consumed by the collection's
|
|
56
101
|
* query/hydration helpers (`getFieldsSync()`, `convertWhereKeys()`,
|
|
57
102
|
* `formatDataJs()`). These come from `fieldsFromClass()` /
|
|
58
103
|
* `ObjectRegistry.getFields()` and only a few members are read here: `type`
|
|
59
|
-
* (for `formatDataJs` value coercion) and the
|
|
60
|
-
*
|
|
61
|
-
*
|
|
104
|
+
* (for `formatDataJs` value coercion) and the field-level access markers. The
|
|
105
|
+
* index signature keeps the bag open for the other registry-attached members
|
|
106
|
+
* without forcing `any`.
|
|
62
107
|
*/
|
|
63
108
|
interface CollectionFieldDefinition {
|
|
64
109
|
type?: string;
|
|
110
|
+
primaryKey?: boolean;
|
|
111
|
+
transient?: boolean;
|
|
65
112
|
sensitive?: boolean;
|
|
113
|
+
readPermission?: string;
|
|
66
114
|
_meta?: {
|
|
67
115
|
sensitive?: boolean;
|
|
116
|
+
readPermission?: string;
|
|
117
|
+
transient?: boolean;
|
|
118
|
+
primaryKey?: boolean;
|
|
119
|
+
__smrtSystemField?: boolean;
|
|
68
120
|
} & Record<string, unknown>;
|
|
69
121
|
[key: string]: unknown;
|
|
70
122
|
}
|
|
@@ -139,6 +191,16 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
139
191
|
* ```
|
|
140
192
|
*/
|
|
141
193
|
private convertWhereKeys;
|
|
194
|
+
private toDbColumnName;
|
|
195
|
+
private assertSafeProjectionFieldName;
|
|
196
|
+
private quoteProjectionIdentifier;
|
|
197
|
+
private isSensitiveFieldDefinition;
|
|
198
|
+
private getReadPermissionFieldDefinition;
|
|
199
|
+
private collectSensitiveFieldNames;
|
|
200
|
+
private hasCustomPrimaryKey;
|
|
201
|
+
private isOmittedCustomPrimaryKeySystemField;
|
|
202
|
+
private resolveProjectionSelect;
|
|
203
|
+
private formatProjectionRow;
|
|
142
204
|
/**
|
|
143
205
|
* Gets the class constructor for items in this collection
|
|
144
206
|
*/
|
|
@@ -299,7 +361,7 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
299
361
|
* The cache key is the final SQL + bound parameters — computed after
|
|
300
362
|
* interceptors (tenancy filters) and STI discriminators are applied, so
|
|
301
363
|
* differently-scoped queries can never share an entry. Cached values are
|
|
302
|
-
* raw rows;
|
|
364
|
+
* raw rows; callers still run their normal post-query formatting path.
|
|
303
365
|
*/
|
|
304
366
|
private queryRowsWithCache;
|
|
305
367
|
/**
|
|
@@ -351,6 +413,7 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
351
413
|
* @param options.offset - Number of records to skip
|
|
352
414
|
* @param options.limit - Maximum number of records to return
|
|
353
415
|
* @param options.orderBy - Field(s) to order results by, with optional direction
|
|
416
|
+
* @param options.select - Column-backed fields to return as plain rows without hydrating model instances
|
|
354
417
|
*
|
|
355
418
|
* @example
|
|
356
419
|
* ```typescript
|
|
@@ -377,46 +440,24 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
377
440
|
* 'last_login <': lastMonth
|
|
378
441
|
* }
|
|
379
442
|
* });
|
|
443
|
+
*
|
|
444
|
+
* // Fetch compact page-key rows without constructing full objects
|
|
445
|
+
* await collection.list({
|
|
446
|
+
* select: ['id', 'title', 'accountId'],
|
|
447
|
+
* where: { status: 'open' },
|
|
448
|
+
* orderBy: 'created_at DESC',
|
|
449
|
+
* limit: 50,
|
|
450
|
+
* });
|
|
380
451
|
* ```
|
|
381
452
|
*
|
|
382
|
-
* @returns Promise resolving to an array of model instances
|
|
453
|
+
* @returns Promise resolving to an array of model instances, or plain selected rows when `select` is present
|
|
383
454
|
*/
|
|
384
|
-
list(options
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
* Relationships to eagerly load (avoids N+1 query problem)
|
|
391
|
-
* @example
|
|
392
|
-
* ```typescript
|
|
393
|
-
* // Load orders with their customers pre-loaded
|
|
394
|
-
* const orders = await orderCollection.list({
|
|
395
|
-
* include: ['customerId']
|
|
396
|
-
* });
|
|
397
|
-
* // Access customer without additional query
|
|
398
|
-
* orders[0].getRelated('customerId');
|
|
399
|
-
* ```
|
|
400
|
-
*/
|
|
401
|
-
include?: string[];
|
|
402
|
-
/**
|
|
403
|
-
* Opt-in read-through cache for this call (issue #1498).
|
|
404
|
-
*
|
|
405
|
-
* Pass `{ ttl }` (milliseconds) to memoize the result rows keyed by
|
|
406
|
-
* the final query shape. Mutations through SMRT invalidate the
|
|
407
|
-
* table's entries automatically. Pass `false` to force a fresh read
|
|
408
|
-
* when the model opted in via `@smrt({ cache })`. Defaults to the
|
|
409
|
-
* model-level config; uncached when neither is set.
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
* ```typescript
|
|
413
|
-
* const published = await resumes.list({
|
|
414
|
-
* where: { status: 'published' },
|
|
415
|
-
* cache: { ttl: 60_000 },
|
|
416
|
-
* });
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
cache?: CollectionCacheConfig | false;
|
|
455
|
+
list<const Select extends readonly SmrtSelectField<ModelType>[]>(options: SmrtListOptions<ModelType> & {
|
|
456
|
+
select: Select;
|
|
457
|
+
include?: never;
|
|
458
|
+
}): Promise<SmrtSelectedRow<ModelType, Select>[]>;
|
|
459
|
+
list(options?: Omit<SmrtListOptions<ModelType>, 'select'> & {
|
|
460
|
+
select?: undefined;
|
|
420
461
|
}): Promise<ModelType[]>;
|
|
421
462
|
/**
|
|
422
463
|
* Eagerly load relationships for a collection of instances
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAEL,KAAK,qBAAqB,EAQ3B,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiD3C;;;GAGG;AACH,KAAK,gBAAgB,GACjB,MAAM,SAAS,GACf,YAAY,GACZ,sBAAsB,GACtB,KAAK,GACL,OAAO,GACP,UAAU,GACV,KAAK,GACL,KAAK,GACL,KAAK,GACL,YAAY,GACZ,SAAS,GACT,WAAW,GACX,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,cAAc,GACd,IAAI,GACJ,IAAI,GACJ,QAAQ,GACR,eAAe,CAAC;AAEpB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,UAAU,IAAI,OAAO,CACzD,IAAI,CACF;KACG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACvD,KAAK,GACL,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACb,EACD,gBAAgB,CACjB,CACF,GAAG;IACF,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5E;;;;;;;;GAQG;AACH,UAAU,yBAAyB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
|
package/dist/collection.js
CHANGED
|
@@ -116,18 +116,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
116
116
|
];
|
|
117
117
|
const fields = this.getFieldsSync();
|
|
118
118
|
const validFieldNames = new Set(Object.keys(fields).map((f) => toSnakeCase(f)));
|
|
119
|
-
const sensitiveFieldNames =
|
|
120
|
-
const collectSensitive = (fieldMap) => {
|
|
121
|
-
const entries = fieldMap instanceof Map ? fieldMap.entries() : Object.entries(fieldMap);
|
|
122
|
-
for (const [fieldName, rawDef] of entries) {
|
|
123
|
-
const def = rawDef;
|
|
124
|
-
if (def && (def.sensitive === true || def._meta?.sensitive === true)) {
|
|
125
|
-
sensitiveFieldNames.add(toSnakeCase(fieldName));
|
|
126
|
-
sensitiveFieldNames.add(fieldName);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
collectSensitive(fields);
|
|
119
|
+
const sensitiveFieldNames = this.collectSensitiveFieldNames(fields);
|
|
131
120
|
validFieldNames.add("id");
|
|
132
121
|
validFieldNames.add("slug");
|
|
133
122
|
validFieldNames.add("context");
|
|
@@ -145,10 +134,10 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
145
134
|
if (ancestorName === "SmrtObject" || ancestorName === "SmrtClass" || ancestorName === itemQualifiedName || ancestorName === itemClassName) continue;
|
|
146
135
|
const ancestorFields = ObjectRegistry.getFields(ancestorName);
|
|
147
136
|
for (const fieldName of ancestorFields.keys()) validFieldNames.add(toSnakeCase(fieldName));
|
|
148
|
-
|
|
137
|
+
this.collectSensitiveFieldNames(ancestorFields, sensitiveFieldNames);
|
|
149
138
|
}
|
|
150
139
|
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
151
|
-
if (stiBase) for (const descendant of ObjectRegistry.getDescendants(stiBase))
|
|
140
|
+
if (stiBase) for (const descendant of ObjectRegistry.getDescendants(stiBase)) this.collectSensitiveFieldNames(ObjectRegistry.getFields(descendant), sensitiveFieldNames);
|
|
152
141
|
}
|
|
153
142
|
const skipFieldValidation = !(Object.keys(fields).length > 0);
|
|
154
143
|
const converted = {};
|
|
@@ -177,6 +166,88 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
177
166
|
}
|
|
178
167
|
return converted;
|
|
179
168
|
}
|
|
169
|
+
toDbColumnName(fieldName) {
|
|
170
|
+
return fieldName.startsWith("_") ? `_${toSnakeCase(fieldName.slice(1))}` : toSnakeCase(fieldName);
|
|
171
|
+
}
|
|
172
|
+
assertSafeProjectionFieldName(fieldName) {
|
|
173
|
+
if (fieldName === "__proto__" || fieldName === "constructor" || fieldName === "prototype") throw new Error(`Invalid select field: '${fieldName}'. Prototype pollution attempts are not allowed.`);
|
|
174
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName)) throw new Error(`Invalid select field: '${fieldName}'. Field names must be identifiers (letters, digits, underscore).`);
|
|
175
|
+
}
|
|
176
|
+
quoteProjectionIdentifier(identifier) {
|
|
177
|
+
this.assertSafeProjectionFieldName(identifier);
|
|
178
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
179
|
+
}
|
|
180
|
+
isSensitiveFieldDefinition(fieldDef) {
|
|
181
|
+
return fieldDef?.sensitive === true || fieldDef?._meta?.sensitive === true;
|
|
182
|
+
}
|
|
183
|
+
getReadPermissionFieldDefinition(fieldDef) {
|
|
184
|
+
if (typeof fieldDef?.readPermission === "string") return fieldDef.readPermission;
|
|
185
|
+
if (typeof fieldDef?._meta?.readPermission === "string") return fieldDef._meta.readPermission;
|
|
186
|
+
}
|
|
187
|
+
collectSensitiveFieldNames(fieldMap, target = /* @__PURE__ */ new Set()) {
|
|
188
|
+
const entries = fieldMap instanceof Map ? fieldMap.entries() : Object.entries(fieldMap);
|
|
189
|
+
for (const [fieldName, fieldDef] of entries) if (this.isSensitiveFieldDefinition(fieldDef)) {
|
|
190
|
+
target.add(toSnakeCase(fieldName));
|
|
191
|
+
target.add(fieldName);
|
|
192
|
+
}
|
|
193
|
+
return target;
|
|
194
|
+
}
|
|
195
|
+
hasCustomPrimaryKey(fields) {
|
|
196
|
+
return Object.values(fields).some((fieldDef) => fieldDef.primaryKey === true || fieldDef._meta?.primaryKey === true);
|
|
197
|
+
}
|
|
198
|
+
isOmittedCustomPrimaryKeySystemField(fieldName, hasCustomPrimaryKey, explicitFieldNames) {
|
|
199
|
+
return hasCustomPrimaryKey && !explicitFieldNames.has(fieldName) && (fieldName === "id" || fieldName === "slug" || fieldName === "context");
|
|
200
|
+
}
|
|
201
|
+
resolveProjectionSelect(select, fields, isSTI) {
|
|
202
|
+
if (!Array.isArray(select)) throw new Error("Invalid select option: expected an array of field names.");
|
|
203
|
+
if (select.length === 0) throw new Error("Invalid select option: at least one field name is required.");
|
|
204
|
+
const customPrimaryKey = this.hasCustomPrimaryKey(fields);
|
|
205
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
206
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
207
|
+
const schema = ObjectRegistry.getSchema(itemQualifiedName) ?? ObjectRegistry.getSchema(itemClassName);
|
|
208
|
+
const schemaColumnNames = schema?.columns ? new Set(Object.keys(schema.columns)) : void 0;
|
|
209
|
+
const registeredFields = ObjectRegistry.getFields(itemQualifiedName);
|
|
210
|
+
const explicitFields = registeredFields.size > 0 ? registeredFields : ObjectRegistry.getFields(itemClassName);
|
|
211
|
+
const explicitFieldNames = new Set(explicitFields.keys());
|
|
212
|
+
const validFieldNames = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
214
|
+
if (this.isOmittedCustomPrimaryKeySystemField(fieldName, customPrimaryKey, explicitFieldNames)) continue;
|
|
215
|
+
const columnName = this.toDbColumnName(fieldName);
|
|
216
|
+
if (schemaColumnNames && !schemaColumnNames.has(columnName)) continue;
|
|
217
|
+
validFieldNames.add(fieldName);
|
|
218
|
+
}
|
|
219
|
+
if (isSTI && (!schemaColumnNames || schemaColumnNames.has("_meta_type"))) validFieldNames.add("_meta_type");
|
|
220
|
+
const seen = /* @__PURE__ */ new Set();
|
|
221
|
+
const outputFields = [];
|
|
222
|
+
const selectExpressions = [];
|
|
223
|
+
for (const fieldName of select) {
|
|
224
|
+
if (typeof fieldName !== "string") throw new Error(`Invalid select field: expected a string field name, got ${typeof fieldName}.`);
|
|
225
|
+
this.assertSafeProjectionFieldName(fieldName);
|
|
226
|
+
if (seen.has(fieldName)) throw new Error(`Invalid select field: '${fieldName}' was requested more than once.`);
|
|
227
|
+
seen.add(fieldName);
|
|
228
|
+
if (!validFieldNames.has(fieldName)) throw new Error(`Invalid select field: '${fieldName}'. Field does not exist on ${itemClassName}. Valid fields: ${Array.from(validFieldNames).sort().join(", ")}`);
|
|
229
|
+
const fieldDef = fields[fieldName];
|
|
230
|
+
const fieldType = fieldDef?.type;
|
|
231
|
+
if (this.isSensitiveFieldDefinition(fieldDef)) throw new Error(`Invalid select field: '${fieldName}' is sensitive and cannot be projected by collection.list({ select }).`);
|
|
232
|
+
const readPermission = this.getReadPermissionFieldDefinition(fieldDef);
|
|
233
|
+
if (readPermission) throw new Error(`Invalid select field: '${fieldName}' is gated by readPermission '${readPermission}' and cannot be projected by collection.list({ select }).`);
|
|
234
|
+
const isTransient = fieldDef?.transient === true || fieldDef?._meta?.transient === true;
|
|
235
|
+
if (fieldType === "meta" || isTransient || fieldType === "oneToMany" || fieldType === "manyToMany") throw new Error(`Invalid select field: '${fieldName}' is not a column-backed field on ${itemClassName}.`);
|
|
236
|
+
const columnName = this.toDbColumnName(fieldName);
|
|
237
|
+
selectExpressions.push(`${this.quoteProjectionIdentifier(columnName)} AS ${this.quoteProjectionIdentifier(fieldName)}`);
|
|
238
|
+
outputFields.push(fieldName);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
sql: selectExpressions.join(", "),
|
|
242
|
+
outputFields
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
formatProjectionRow(row, fields, outputFields) {
|
|
246
|
+
const formattedData = this.withHydratedCoreFields(formatDataJs(row, fields));
|
|
247
|
+
const projected = {};
|
|
248
|
+
for (const fieldName of outputFields) projected[fieldName] = formattedData[fieldName];
|
|
249
|
+
return projected;
|
|
250
|
+
}
|
|
180
251
|
/**
|
|
181
252
|
* Gets the class constructor for items in this collection
|
|
182
253
|
*/
|
|
@@ -412,7 +483,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
412
483
|
* The cache key is the final SQL + bound parameters — computed after
|
|
413
484
|
* interceptors (tenancy filters) and STI discriminators are applied, so
|
|
414
485
|
* differently-scoped queries can never share an entry. Cached values are
|
|
415
|
-
* raw rows;
|
|
486
|
+
* raw rows; callers still run their normal post-query formatting path.
|
|
416
487
|
*/
|
|
417
488
|
async queryRowsWithCache(sql, params, cacheConfig) {
|
|
418
489
|
if (!cacheConfig) return (await this.db.query(sql, ...params)).rows;
|
|
@@ -492,52 +563,13 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
492
563
|
const instance = await this.hydrateResultRow(rows[0], fields, isSTI);
|
|
493
564
|
return await GlobalInterceptors.executeAfterGet(itemClassName, instance, interceptorContext);
|
|
494
565
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Lists records from the collection with flexible filtering options
|
|
497
|
-
*
|
|
498
|
-
* @param options - Query options object
|
|
499
|
-
* @param options.where - Record of conditions to filter results. Each key can include an operator
|
|
500
|
-
* separated by a space (e.g., 'price >', 'name like'). Default operator is '='.
|
|
501
|
-
* @param options.offset - Number of records to skip
|
|
502
|
-
* @param options.limit - Maximum number of records to return
|
|
503
|
-
* @param options.orderBy - Field(s) to order results by, with optional direction
|
|
504
|
-
*
|
|
505
|
-
* @example
|
|
506
|
-
* ```typescript
|
|
507
|
-
* // Find active products priced between $100-$200
|
|
508
|
-
* await collection.list({
|
|
509
|
-
* where: {
|
|
510
|
-
* 'price >': 100,
|
|
511
|
-
* 'price <=': 200,
|
|
512
|
-
* 'status': 'active', // equals operator is default
|
|
513
|
-
* 'category in': ['A', 'B', 'C'], // IN operator for arrays
|
|
514
|
-
* 'name like': '%shirt%', // LIKE for pattern matching
|
|
515
|
-
* 'deleted_at !=': null // exclude deleted items
|
|
516
|
-
* },
|
|
517
|
-
* limit: 10,
|
|
518
|
-
* offset: 0
|
|
519
|
-
* });
|
|
520
|
-
*
|
|
521
|
-
* // Find users matching pattern but not in specific roles
|
|
522
|
-
* await users.list({
|
|
523
|
-
* where: {
|
|
524
|
-
* 'email like': '%@company.com',
|
|
525
|
-
* 'active': true,
|
|
526
|
-
* 'role in': ['guest', 'blocked'],
|
|
527
|
-
* 'last_login <': lastMonth
|
|
528
|
-
* }
|
|
529
|
-
* });
|
|
530
|
-
* ```
|
|
531
|
-
*
|
|
532
|
-
* @returns Promise resolving to an array of model instances
|
|
533
|
-
*/
|
|
534
566
|
async list(options = {}) {
|
|
535
567
|
await this.ensureStorageReady();
|
|
536
568
|
const itemClassName = this.getResolvedItemClassName();
|
|
537
569
|
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
538
570
|
const interceptorContext = createInterceptorContext(itemClassName, "list", this.constructor.name);
|
|
539
571
|
const interceptedOptions = await GlobalInterceptors.executeBeforeList(itemClassName, options, interceptorContext) ?? options ?? {};
|
|
540
|
-
let { where, offset, limit, orderBy } = interceptedOptions;
|
|
572
|
+
let { where, offset, limit, orderBy, select } = interceptedOptions;
|
|
541
573
|
const isSTI = ObjectRegistry.getTableStrategy(itemQualifiedName) === "sti";
|
|
542
574
|
if (isSTI) {
|
|
543
575
|
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
@@ -548,6 +580,9 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
548
580
|
}
|
|
549
581
|
where = resolveMetaTypeInWhere(where);
|
|
550
582
|
const { sql: whereSql, values: whereValues } = buildWhere(this.convertWhereKeys(where || {}));
|
|
583
|
+
const fields = this.getFieldsSync();
|
|
584
|
+
const projection = select !== void 0 ? this.resolveProjectionSelect(select, fields, isSTI) : void 0;
|
|
585
|
+
if (projection && interceptedOptions.include && interceptedOptions.include.length > 0) throw new Error("collection.list({ select }) returns plain projection rows and cannot eager-load relationships. Remove include or omit select.");
|
|
551
586
|
let orderBySql = "";
|
|
552
587
|
if (orderBy) {
|
|
553
588
|
orderBySql = " ORDER BY ";
|
|
@@ -570,10 +605,10 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
570
605
|
limitOffsetSql += ` OFFSET $${paramIndex++}`;
|
|
571
606
|
limitOffsetValues.push(offset);
|
|
572
607
|
}
|
|
573
|
-
const sql = `SELECT * FROM ${this.tableName} ${whereSql} ${orderBySql} ${limitOffsetSql}`;
|
|
608
|
+
const sql = `SELECT ${projection ? projection.sql : "*"} FROM ${this.tableName} ${whereSql} ${orderBySql} ${limitOffsetSql}`;
|
|
574
609
|
const params = [...whereValues, ...limitOffsetValues];
|
|
575
610
|
const rows = await this.queryRowsWithCache(sql, params, this.resolveReadCacheConfig(interceptedOptions.cache));
|
|
576
|
-
|
|
611
|
+
if (projection) return rows.map((item) => this.formatProjectionRow(item, fields, projection.outputFields));
|
|
577
612
|
const instances = await Promise.all(rows.map((item) => this.hydrateResultRow(item, fields, isSTI)));
|
|
578
613
|
if (interceptedOptions.include && interceptedOptions.include.length > 0) await this.eagerLoadRelationships(instances, interceptedOptions.include);
|
|
579
614
|
return await GlobalInterceptors.executeAfterList(itemClassName, instances, interceptorContext);
|