@stndrds/schema 0.1.0-alpha.20 → 0.1.0-alpha.22
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/dist/chunk-3RVLIIUK.mjs +9257 -0
- package/dist/chunk-6FNFXEQY.mjs +9316 -0
- package/dist/chunk-6TJR4WDK.mjs +9262 -0
- package/dist/chunk-7GLSGSUP.mjs +9456 -0
- package/dist/chunk-B74KNXDP.mjs +9457 -0
- package/dist/chunk-F27AKBTL.mjs +9452 -0
- package/dist/chunk-HSNBVKQU.mjs +9316 -0
- package/dist/chunk-IKUIWNHZ.mjs +9460 -0
- package/dist/chunk-JJLODIBK.mjs +9389 -0
- package/dist/chunk-MAXEN3GU.mjs +9266 -0
- package/dist/chunk-ZD3BFM25.mjs +9316 -0
- package/dist/index.d.mts +3 -197
- package/dist/index.d.ts +3 -197
- package/dist/index.js +632 -21
- package/dist/index.mjs +21 -1
- package/dist/runtime-0gaIqXql.d.mts +7074 -0
- package/dist/runtime-0gaIqXql.d.ts +7074 -0
- package/dist/runtime-3u5doYAK.d.mts +6934 -0
- package/dist/runtime-3u5doYAK.d.ts +6934 -0
- package/dist/runtime-B6mOrMV4.d.mts +6985 -0
- package/dist/runtime-B6mOrMV4.d.ts +6985 -0
- package/dist/runtime-BNML-fPx.d.mts +6969 -0
- package/dist/runtime-BNML-fPx.d.ts +6969 -0
- package/dist/runtime-BPdsKDJo.d.mts +6969 -0
- package/dist/runtime-BPdsKDJo.d.ts +6969 -0
- package/dist/runtime-BZZRu7A1.d.mts +7083 -0
- package/dist/runtime-BZZRu7A1.d.ts +7083 -0
- package/dist/runtime-CmYcicVr.d.mts +7083 -0
- package/dist/runtime-CmYcicVr.d.ts +7083 -0
- package/dist/runtime-D1jyS1lS.d.mts +7008 -0
- package/dist/runtime-D1jyS1lS.d.ts +7008 -0
- package/dist/runtime-DjEUAeL4.d.mts +6713 -0
- package/dist/runtime-DjEUAeL4.d.ts +6713 -0
- package/dist/runtime-rTM9ddEe.d.mts +6934 -0
- package/dist/runtime-rTM9ddEe.d.ts +6934 -0
- package/dist/runtime-xLzAkAnp.d.mts +6969 -0
- package/dist/runtime-xLzAkAnp.d.ts +6969 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +620 -21
- package/dist/runtime.mjs +17 -1
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -134,16 +134,22 @@ __export(runtime_exports, {
|
|
|
134
134
|
NoopHookRegistry: () => NoopHookRegistry,
|
|
135
135
|
ObjectSchemaService: () => ObjectSchemaService,
|
|
136
136
|
PermissionService: () => PermissionService,
|
|
137
|
+
QueryBuilder: () => QueryBuilder,
|
|
138
|
+
QueryMultipleResultsError: () => QueryMultipleResultsError,
|
|
139
|
+
QueryNoResultError: () => QueryNoResultError,
|
|
137
140
|
RecordService: () => RecordService,
|
|
138
141
|
RelationResolverService: () => RelationResolverService,
|
|
139
142
|
RelationService: () => RelationService,
|
|
140
143
|
RollupScheduler: () => RollupScheduler,
|
|
141
144
|
RollupService: () => RollupService,
|
|
145
|
+
SHORTCUT_TO_FILTER_OPERATOR: () => SHORTCUT_TO_FILTER_OPERATOR,
|
|
142
146
|
UserProfileService: () => UserProfileService,
|
|
143
147
|
UserService: () => UserService,
|
|
144
148
|
ViewService: () => ViewService,
|
|
145
149
|
buildAuditChanges: () => buildAuditChanges,
|
|
150
|
+
createDefaultState: () => createDefaultState,
|
|
146
151
|
createMockAdapter: () => createMockAdapter,
|
|
152
|
+
createQueryBuilder: () => createQueryBuilder,
|
|
147
153
|
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
148
154
|
evaluateFormula: () => evaluateFormula,
|
|
149
155
|
evaluateFormulaAttribute: () => evaluateFormulaAttribute,
|
|
@@ -156,6 +162,8 @@ __export(runtime_exports, {
|
|
|
156
162
|
extractRelationReferences: () => extractRelationReferences,
|
|
157
163
|
flattenRelationsForEval: () => flattenRelationsForEval,
|
|
158
164
|
formatFormulaResult: () => formatFormulaResult,
|
|
165
|
+
formatRecord: () => formatRecord,
|
|
166
|
+
formatRecords: () => formatRecords,
|
|
159
167
|
getPathDepth: () => getPathDepth,
|
|
160
168
|
getRelationPath: () => getRelationPath,
|
|
161
169
|
getSyncPreview: () => getSyncPreview,
|
|
@@ -179,6 +187,477 @@ __export(runtime_exports, {
|
|
|
179
187
|
});
|
|
180
188
|
module.exports = __toCommonJS(runtime_exports);
|
|
181
189
|
|
|
190
|
+
// src/runtime/client/types.ts
|
|
191
|
+
function formatRecord(record) {
|
|
192
|
+
return {
|
|
193
|
+
id: record.id,
|
|
194
|
+
createdAt: record.createdAt,
|
|
195
|
+
updatedAt: record.updatedAt,
|
|
196
|
+
metadata: record.metadata ?? {},
|
|
197
|
+
...record.values
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function formatRecords(records) {
|
|
201
|
+
return records.map((r) => formatRecord(r));
|
|
202
|
+
}
|
|
203
|
+
function createDefaultState(objectName) {
|
|
204
|
+
return {
|
|
205
|
+
objectName,
|
|
206
|
+
filters: [],
|
|
207
|
+
combinator: "and",
|
|
208
|
+
sorts: [],
|
|
209
|
+
raw: false,
|
|
210
|
+
includeDeleted: false
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var SHORTCUT_TO_FILTER_OPERATOR = {
|
|
214
|
+
eq: "is",
|
|
215
|
+
neq: "is_not",
|
|
216
|
+
gt: "gt",
|
|
217
|
+
gte: "gte",
|
|
218
|
+
lt: "lt",
|
|
219
|
+
lte: "lte",
|
|
220
|
+
contains: "contains",
|
|
221
|
+
notContains: "not_contains",
|
|
222
|
+
startsWith: "starts_with",
|
|
223
|
+
endsWith: "ends_with",
|
|
224
|
+
isEmpty: "is_empty",
|
|
225
|
+
isNotEmpty: "is_not_empty"
|
|
226
|
+
};
|
|
227
|
+
var QueryNoResultError = class extends Error {
|
|
228
|
+
constructor(objectName, filters) {
|
|
229
|
+
const filterInfo = filters?.length ? ` with filters: ${JSON.stringify(filters)}` : "";
|
|
230
|
+
super(`No record found in "${objectName}"${filterInfo}`);
|
|
231
|
+
this.name = "QueryNoResultError";
|
|
232
|
+
this.objectName = objectName;
|
|
233
|
+
this.filters = filters;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var QueryMultipleResultsError = class extends Error {
|
|
237
|
+
constructor(objectName, count) {
|
|
238
|
+
super(
|
|
239
|
+
`Expected single record in "${objectName}", but found ${count}. Use first() or add more filters.`
|
|
240
|
+
);
|
|
241
|
+
this.name = "QueryMultipleResultsError";
|
|
242
|
+
this.objectName = objectName;
|
|
243
|
+
this.count = count;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/runtime/client/query-builder.ts
|
|
248
|
+
var QueryBuilder = class _QueryBuilder {
|
|
249
|
+
constructor(recordService, adapter, objectName, state) {
|
|
250
|
+
this.objectId = null;
|
|
251
|
+
this.recordService = recordService;
|
|
252
|
+
this.adapter = adapter;
|
|
253
|
+
this.state = {
|
|
254
|
+
...createDefaultState(objectName),
|
|
255
|
+
...state
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Clone the builder with new state (immutability pattern)
|
|
260
|
+
*/
|
|
261
|
+
clone(updates) {
|
|
262
|
+
return new _QueryBuilder(this.recordService, this.adapter, this.state.objectName, {
|
|
263
|
+
...this.state,
|
|
264
|
+
...updates
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve object name to object ID (cached)
|
|
269
|
+
*/
|
|
270
|
+
async resolveObjectId() {
|
|
271
|
+
if (this.objectId) return this.objectId;
|
|
272
|
+
const tenantId = this.recordService.tenantId;
|
|
273
|
+
const dbObject = await this.adapter.objects.findByName(tenantId, this.state.objectName);
|
|
274
|
+
if (!dbObject) {
|
|
275
|
+
throw new Error(`Object "${this.state.objectName}" not found`);
|
|
276
|
+
}
|
|
277
|
+
this.objectId = dbObject.id;
|
|
278
|
+
return dbObject.id;
|
|
279
|
+
}
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// FILTER METHODS
|
|
282
|
+
// ============================================================================
|
|
283
|
+
/**
|
|
284
|
+
* Add a filter rule with explicit operator
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* qb.where('status', 'is', 'active')
|
|
289
|
+
* .where('price', 'gte', 100)
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
where(attribute, operator, value) {
|
|
293
|
+
const rule = { attribute, operator, value };
|
|
294
|
+
return this.clone({
|
|
295
|
+
filters: [...this.state.filters, rule]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Set filter combinator (default: 'and')
|
|
300
|
+
*/
|
|
301
|
+
or() {
|
|
302
|
+
return this.clone({ combinator: "or" });
|
|
303
|
+
}
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// SHORTCUT OPERATORS
|
|
306
|
+
// ============================================================================
|
|
307
|
+
/**
|
|
308
|
+
* Equal (is)
|
|
309
|
+
*/
|
|
310
|
+
eq(attribute, value) {
|
|
311
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.eq, value);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Not equal (is_not)
|
|
315
|
+
*/
|
|
316
|
+
neq(attribute, value) {
|
|
317
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.neq, value);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Greater than
|
|
321
|
+
*/
|
|
322
|
+
gt(attribute, value) {
|
|
323
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gt, value);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Greater than or equal
|
|
327
|
+
*/
|
|
328
|
+
gte(attribute, value) {
|
|
329
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gte, value);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Less than
|
|
333
|
+
*/
|
|
334
|
+
lt(attribute, value) {
|
|
335
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lt, value);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Less than or equal
|
|
339
|
+
*/
|
|
340
|
+
lte(attribute, value) {
|
|
341
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lte, value);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Contains (text/relation)
|
|
345
|
+
*/
|
|
346
|
+
contains(attribute, value) {
|
|
347
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.contains, value);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Not contains
|
|
351
|
+
*/
|
|
352
|
+
notContains(attribute, value) {
|
|
353
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.notContains, value);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Starts with (text)
|
|
357
|
+
*/
|
|
358
|
+
startsWith(attribute, value) {
|
|
359
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.startsWith, value);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Ends with (text)
|
|
363
|
+
*/
|
|
364
|
+
endsWith(attribute, value) {
|
|
365
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.endsWith, value);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Is empty
|
|
369
|
+
*/
|
|
370
|
+
isEmpty(attribute) {
|
|
371
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isEmpty, null);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Is not empty
|
|
375
|
+
*/
|
|
376
|
+
isNotEmpty(attribute) {
|
|
377
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isNotEmpty, null);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* In list (any_of)
|
|
381
|
+
*/
|
|
382
|
+
in(attribute, values) {
|
|
383
|
+
return this.where(attribute, "any_of", values);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Not in list (none_of)
|
|
387
|
+
*/
|
|
388
|
+
notIn(attribute, values) {
|
|
389
|
+
return this.where(attribute, "none_of", values);
|
|
390
|
+
}
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// SORT & PAGINATION
|
|
393
|
+
// ============================================================================
|
|
394
|
+
/**
|
|
395
|
+
* Add sort rule
|
|
396
|
+
*/
|
|
397
|
+
orderBy(attribute, direction = "asc") {
|
|
398
|
+
return this.clone({
|
|
399
|
+
sorts: [...this.state.sorts, { attribute, direction }]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Set limit
|
|
404
|
+
*/
|
|
405
|
+
limit(count) {
|
|
406
|
+
return this.clone({ limit: count });
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Set offset
|
|
410
|
+
*/
|
|
411
|
+
offset(count) {
|
|
412
|
+
return this.clone({ offset: count });
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Include soft-deleted records
|
|
416
|
+
*/
|
|
417
|
+
withDeleted() {
|
|
418
|
+
return this.clone({ includeDeleted: true });
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// GROUP BY (KANBAN)
|
|
422
|
+
// ============================================================================
|
|
423
|
+
/**
|
|
424
|
+
* Group results by an attribute (for Kanban views).
|
|
425
|
+
* Only status and select attributes are supported.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* const grouped = await qb
|
|
430
|
+
* .groupBy('status')
|
|
431
|
+
* .fetchGrouped();
|
|
432
|
+
* // → { groups: { 'pending': [...], 'active': [...] }, total: 50 }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
groupBy(attribute) {
|
|
436
|
+
return this.clone({ groupBy: attribute });
|
|
437
|
+
}
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// RAW MODE
|
|
440
|
+
// ============================================================================
|
|
441
|
+
/**
|
|
442
|
+
* Return raw ObjectRecord instead of formatted record.
|
|
443
|
+
* Includes all metadata (objectId, label, completionStatus, etc.)
|
|
444
|
+
*/
|
|
445
|
+
raw() {
|
|
446
|
+
return this.clone({ raw: true });
|
|
447
|
+
}
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// TERMINATION METHODS (READ)
|
|
450
|
+
// ============================================================================
|
|
451
|
+
/**
|
|
452
|
+
* Fetch records with current filters
|
|
453
|
+
* Returns formatted records by default, raw ObjectRecords if raw() was called
|
|
454
|
+
*/
|
|
455
|
+
async fetch() {
|
|
456
|
+
const objectId = await this.resolveObjectId();
|
|
457
|
+
const filters = this.state.filters.length > 0 ? { combinator: this.state.combinator, rules: this.state.filters } : void 0;
|
|
458
|
+
const result = await this.recordService.listRecords(objectId, {
|
|
459
|
+
filters,
|
|
460
|
+
sorts: this.state.sorts.length > 0 ? this.state.sorts : void 0,
|
|
461
|
+
limit: this.state.limit,
|
|
462
|
+
offset: this.state.offset,
|
|
463
|
+
includeDeleted: this.state.includeDeleted
|
|
464
|
+
});
|
|
465
|
+
if (this.state.raw) {
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
records: formatRecords(result.records),
|
|
470
|
+
total: result.total
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get a single record (throws if 0 or >1 results)
|
|
475
|
+
*/
|
|
476
|
+
async single() {
|
|
477
|
+
const result = await this.limit(2).fetch();
|
|
478
|
+
if (result.records.length === 0) {
|
|
479
|
+
throw new QueryNoResultError(this.state.objectName, this.state.filters);
|
|
480
|
+
}
|
|
481
|
+
if (result.records.length > 1) {
|
|
482
|
+
throw new QueryMultipleResultsError(this.state.objectName, result.total);
|
|
483
|
+
}
|
|
484
|
+
return result.records[0];
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get first record or null
|
|
488
|
+
*/
|
|
489
|
+
async first() {
|
|
490
|
+
const result = await this.limit(1).fetch();
|
|
491
|
+
return result.records[0] ?? null;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get record by ID
|
|
495
|
+
*/
|
|
496
|
+
async findById(id) {
|
|
497
|
+
const record = await this.recordService.getRecord(id);
|
|
498
|
+
if (!record) return null;
|
|
499
|
+
if (this.state.raw) {
|
|
500
|
+
return record;
|
|
501
|
+
}
|
|
502
|
+
return formatRecord(record);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Count records matching filters
|
|
506
|
+
*/
|
|
507
|
+
async count() {
|
|
508
|
+
const result = await this.limit(0).fetch();
|
|
509
|
+
return result.total;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Fetch records grouped by the groupBy attribute (for Kanban views).
|
|
513
|
+
* Requires groupBy() to be called first.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const result = await schema.from('tasks')
|
|
518
|
+
* .groupBy('status')
|
|
519
|
+
* .fetchGrouped();
|
|
520
|
+
* // → { groups: { 'todo': [...], 'in_progress': [...], 'done': [...] }, total: 50 }
|
|
521
|
+
* ```
|
|
522
|
+
*/
|
|
523
|
+
async fetchGrouped() {
|
|
524
|
+
if (!this.state.groupBy) {
|
|
525
|
+
throw new Error("groupBy() must be called before fetchGrouped()");
|
|
526
|
+
}
|
|
527
|
+
const result = await this.fetch();
|
|
528
|
+
const groupByAttr = this.state.groupBy;
|
|
529
|
+
const groups = {};
|
|
530
|
+
for (const record of result.records) {
|
|
531
|
+
const value = record[groupByAttr];
|
|
532
|
+
const key = value != null ? String(value) : "__null__";
|
|
533
|
+
if (!groups[key]) {
|
|
534
|
+
groups[key] = [];
|
|
535
|
+
}
|
|
536
|
+
groups[key].push(record);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
groups,
|
|
540
|
+
total: result.total
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// TERMINATION METHODS (WRITE)
|
|
545
|
+
// ============================================================================
|
|
546
|
+
/**
|
|
547
|
+
* Create a new record
|
|
548
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* const product = await schema.from('products')
|
|
553
|
+
* .insert({ name: 'iPhone', price: 999 });
|
|
554
|
+
*
|
|
555
|
+
* // Allow draft (missing required fields)
|
|
556
|
+
* const draft = await schema.from('products')
|
|
557
|
+
* .insert({ name: 'Draft' }, { allowDraft: true });
|
|
558
|
+
*
|
|
559
|
+
* // With custom metadata
|
|
560
|
+
* const withMeta = await schema.from('products')
|
|
561
|
+
* .insert({ name: 'iPhone' }, { metadata: { externalId: 'ext-123' } });
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
async insert(data, options) {
|
|
565
|
+
const objectId = await this.resolveObjectId();
|
|
566
|
+
const record = await this.recordService.createRecord(
|
|
567
|
+
objectId,
|
|
568
|
+
data,
|
|
569
|
+
{
|
|
570
|
+
allowDraft: options?.allowDraft,
|
|
571
|
+
validate: options?.validate,
|
|
572
|
+
metadata: options?.metadata
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
if (this.state.raw) {
|
|
576
|
+
return record;
|
|
577
|
+
}
|
|
578
|
+
return formatRecord(record);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Update records matching filters (expects exactly 1 record by default)
|
|
582
|
+
* Use with eq('id', ...) for single record update.
|
|
583
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* const updated = await schema.from('products')
|
|
588
|
+
* .eq('id', 'rec-123')
|
|
589
|
+
* .update({ price: 899 });
|
|
590
|
+
*
|
|
591
|
+
* // Update with metadata (merged with existing, set undefined to remove)
|
|
592
|
+
* const withMeta = await schema.from('products')
|
|
593
|
+
* .eq('id', 'rec-123')
|
|
594
|
+
* .update({ price: 899 }, { metadata: { synced: true, oldKey: undefined } });
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
async update(data, options) {
|
|
598
|
+
const existing = await this.single();
|
|
599
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
600
|
+
const record = await this.recordService.updateRecord(
|
|
601
|
+
recordId,
|
|
602
|
+
data,
|
|
603
|
+
{
|
|
604
|
+
partial: true,
|
|
605
|
+
metadata: options?.metadata
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
if (this.state.raw) {
|
|
609
|
+
return record;
|
|
610
|
+
}
|
|
611
|
+
return formatRecord(record);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Delete record matching filters (expects exactly 1 record)
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```typescript
|
|
618
|
+
* await schema.from('products')
|
|
619
|
+
* .eq('id', 'rec-123')
|
|
620
|
+
* .delete();
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
async delete() {
|
|
624
|
+
const existing = await this.single();
|
|
625
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
626
|
+
await this.recordService.deleteRecord(recordId);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Upsert (update if exists by ID, insert otherwise)
|
|
630
|
+
* Provide 'id' for lookup. System fields (createdAt, updatedAt) are managed automatically.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```typescript
|
|
634
|
+
* // Insert if not exists, update if exists
|
|
635
|
+
* const product = await schema.from('products')
|
|
636
|
+
* .upsert({ id: 'rec-123', name: 'iPhone', price: 999 });
|
|
637
|
+
*
|
|
638
|
+
* // With metadata
|
|
639
|
+
* const withMeta = await schema.from('products')
|
|
640
|
+
* .upsert({ name: 'iPhone' }, { metadata: { source: 'import' } });
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
async upsert(data, options) {
|
|
644
|
+
const { id, ...rest } = data;
|
|
645
|
+
const writeData = rest;
|
|
646
|
+
if (id) {
|
|
647
|
+
const existing = await this.findById(id);
|
|
648
|
+
if (existing) {
|
|
649
|
+
return this.eq("id", id).update(writeData, {
|
|
650
|
+
metadata: options?.metadata
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return this.insert(writeData, options);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
function createQueryBuilder(recordService, adapter, objectName) {
|
|
658
|
+
return new QueryBuilder(recordService, adapter, objectName);
|
|
659
|
+
}
|
|
660
|
+
|
|
182
661
|
// src/runtime/formula/evaluator.ts
|
|
183
662
|
var import_expr_eval = require("expr-eval");
|
|
184
663
|
function createFormulaParser() {
|
|
@@ -1201,6 +1680,7 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
1201
1680
|
label: data.label,
|
|
1202
1681
|
completionStatus: data.completionStatus,
|
|
1203
1682
|
values: data.data,
|
|
1683
|
+
metadata: data.metadata ?? {},
|
|
1204
1684
|
createdAt: /* @__PURE__ */ new Date(),
|
|
1205
1685
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1206
1686
|
};
|
|
@@ -1213,11 +1693,13 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
1213
1693
|
if (!existing) {
|
|
1214
1694
|
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
1215
1695
|
}
|
|
1216
|
-
const { __completionStatus, __label, ...valueData } = data;
|
|
1696
|
+
const { __completionStatus, __label, __metadata, ...valueData } = data;
|
|
1217
1697
|
const updated = {
|
|
1218
1698
|
...existing,
|
|
1219
1699
|
label: __label ?? existing.label,
|
|
1220
1700
|
completionStatus: __completionStatus ?? existing.completionStatus,
|
|
1701
|
+
// Metadata uses replace behavior (not merge)
|
|
1702
|
+
metadata: __metadata !== void 0 ? __metadata : existing.metadata,
|
|
1221
1703
|
values: { ...existing.values, ...valueData },
|
|
1222
1704
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1223
1705
|
};
|
|
@@ -3415,7 +3897,7 @@ function createCheckboxValidator(_attr) {
|
|
|
3415
3897
|
return import_zod4.z.boolean();
|
|
3416
3898
|
}
|
|
3417
3899
|
function createDateValidator(attr) {
|
|
3418
|
-
return import_zod4.z.
|
|
3900
|
+
return import_zod4.z.coerce.date({ message: `${attr.label} must be a valid ISO date` });
|
|
3419
3901
|
}
|
|
3420
3902
|
function createPhoneValidator(_attr) {
|
|
3421
3903
|
return import_zod4.z.object({
|
|
@@ -3521,7 +4003,7 @@ function createRelationValidator(attr) {
|
|
|
3521
4003
|
return createSingleRelationValidator(attr);
|
|
3522
4004
|
}
|
|
3523
4005
|
function createRatingValidator(attr) {
|
|
3524
|
-
let schema = import_zod4.z.number().
|
|
4006
|
+
let schema = import_zod4.z.number().min(0);
|
|
3525
4007
|
if (attr.max !== void 0) {
|
|
3526
4008
|
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
3527
4009
|
}
|
|
@@ -3675,7 +4157,7 @@ function computeRecordStatus(objectDef, data) {
|
|
|
3675
4157
|
}
|
|
3676
4158
|
|
|
3677
4159
|
// src/runtime/services/object-schema.service.ts
|
|
3678
|
-
var
|
|
4160
|
+
var _ObjectSchemaService = class _ObjectSchemaService {
|
|
3679
4161
|
constructor(adapter, nativeRegistry, options) {
|
|
3680
4162
|
this.adapter = adapter;
|
|
3681
4163
|
this.nativeRegistry = nativeRegistry;
|
|
@@ -4098,6 +4580,14 @@ var ObjectSchemaService = class {
|
|
|
4098
4580
|
"Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
|
|
4099
4581
|
);
|
|
4100
4582
|
}
|
|
4583
|
+
if (_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.includes(
|
|
4584
|
+
name
|
|
4585
|
+
)) {
|
|
4586
|
+
throw new Error(
|
|
4587
|
+
`"${name}" is a reserved name and cannot be used as an attribute name.
|
|
4588
|
+
Reserved names: ${_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.join(", ")}`
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4101
4591
|
}
|
|
4102
4592
|
/**
|
|
4103
4593
|
* Get complete object schema (system + custom attributes)
|
|
@@ -4344,6 +4834,17 @@ var ObjectSchemaService = class {
|
|
|
4344
4834
|
return baseAttr;
|
|
4345
4835
|
}
|
|
4346
4836
|
};
|
|
4837
|
+
/**
|
|
4838
|
+
* Reserved attribute names that cannot be used.
|
|
4839
|
+
* These names conflict with system fields on ObjectRecord.
|
|
4840
|
+
*/
|
|
4841
|
+
_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES = [
|
|
4842
|
+
"id",
|
|
4843
|
+
"createdAt",
|
|
4844
|
+
"updatedAt",
|
|
4845
|
+
"metadata"
|
|
4846
|
+
];
|
|
4847
|
+
var ObjectSchemaService = _ObjectSchemaService;
|
|
4347
4848
|
|
|
4348
4849
|
// src/runtime/services/permission.service.ts
|
|
4349
4850
|
var PermissionService = class {
|
|
@@ -5303,6 +5804,9 @@ var RollupService = class {
|
|
|
5303
5804
|
* Extract and aggregate values from related records
|
|
5304
5805
|
*/
|
|
5305
5806
|
aggregateValues(relatedRecords, rollupAttr) {
|
|
5807
|
+
if (rollupAttr.function === "count") {
|
|
5808
|
+
return { value: relatedRecords.length, recordCount: relatedRecords.length };
|
|
5809
|
+
}
|
|
5306
5810
|
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
5307
5811
|
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
5308
5812
|
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
@@ -5451,6 +5955,71 @@ var RollupService = class {
|
|
|
5451
5955
|
}
|
|
5452
5956
|
return [...new Set(affectedIds)];
|
|
5453
5957
|
}
|
|
5958
|
+
/**
|
|
5959
|
+
* Find records that have forward rollups pointing to the modified record.
|
|
5960
|
+
*
|
|
5961
|
+
* Forward rollups are rollups where the record has a relation attribute
|
|
5962
|
+
* pointing to another object, and the rollup aggregates values from that target.
|
|
5963
|
+
* When the target record changes, we need to recalculate these rollups.
|
|
5964
|
+
*
|
|
5965
|
+
* Example: Order has relation "company" → Company, and rollup "capitalSocial"
|
|
5966
|
+
* aggregating from the Company. When Company.capitalSocial changes,
|
|
5967
|
+
* all Orders pointing to that Company need their rollup recalculated.
|
|
5968
|
+
*
|
|
5969
|
+
* @param changedRecord - The record that was modified
|
|
5970
|
+
* @param changedSchema - Schema of the changed record's object
|
|
5971
|
+
* @returns Array of records that need their forward rollups recalculated
|
|
5972
|
+
*/
|
|
5973
|
+
async findRecordsWithForwardRollup(changedRecord, changedSchema) {
|
|
5974
|
+
const affectedRecords = [];
|
|
5975
|
+
const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
|
|
5976
|
+
if (!recordObject) {
|
|
5977
|
+
return affectedRecords;
|
|
5978
|
+
}
|
|
5979
|
+
const tenantId = recordObject.tenantId;
|
|
5980
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
5981
|
+
for (const obj of allObjects) {
|
|
5982
|
+
if (obj.id === changedRecord.objectId) {
|
|
5983
|
+
continue;
|
|
5984
|
+
}
|
|
5985
|
+
const attributes = await this.adapter.attributes.findByObjectId(obj.id);
|
|
5986
|
+
const rollupAttrs = attributes.filter((a) => a.type === "rollup");
|
|
5987
|
+
if (rollupAttrs.length === 0) {
|
|
5988
|
+
continue;
|
|
5989
|
+
}
|
|
5990
|
+
for (const rollupDbAttr of rollupAttrs) {
|
|
5991
|
+
const rollupConfig = rollupDbAttr.config;
|
|
5992
|
+
if (!rollupConfig?.relationAttribute) {
|
|
5993
|
+
continue;
|
|
5994
|
+
}
|
|
5995
|
+
const relationAttr = attributes.find(
|
|
5996
|
+
(a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
|
|
5997
|
+
);
|
|
5998
|
+
if (!relationAttr) {
|
|
5999
|
+
continue;
|
|
6000
|
+
}
|
|
6001
|
+
const relationConfig = relationAttr.config;
|
|
6002
|
+
const targetsChangedObject = relationConfig?.targets?.some(
|
|
6003
|
+
(t) => t.object === changedSchema.name
|
|
6004
|
+
);
|
|
6005
|
+
if (!targetsChangedObject) {
|
|
6006
|
+
continue;
|
|
6007
|
+
}
|
|
6008
|
+
const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
|
|
6009
|
+
tenantId,
|
|
6010
|
+
obj.id,
|
|
6011
|
+
relationAttr.name,
|
|
6012
|
+
changedRecord.id
|
|
6013
|
+
);
|
|
6014
|
+
affectedRecords.push(...recordsPointingToChanged);
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
const uniqueRecords = /* @__PURE__ */ new Map();
|
|
6018
|
+
for (const record of affectedRecords) {
|
|
6019
|
+
uniqueRecords.set(record.id, record);
|
|
6020
|
+
}
|
|
6021
|
+
return Array.from(uniqueRecords.values());
|
|
6022
|
+
}
|
|
5454
6023
|
};
|
|
5455
6024
|
|
|
5456
6025
|
// src/runtime/services/user.service.ts
|
|
@@ -5575,7 +6144,11 @@ var RecordService = class {
|
|
|
5575
6144
|
constructor(adapter, tenantId, options) {
|
|
5576
6145
|
this.adapter = adapter;
|
|
5577
6146
|
this.tenantId = tenantId;
|
|
5578
|
-
this.schemaService = new ObjectSchemaService(adapter, registry
|
|
6147
|
+
this.schemaService = new ObjectSchemaService(adapter, registry, {
|
|
6148
|
+
userId: options?.userId,
|
|
6149
|
+
userEmail: options?.userEmail,
|
|
6150
|
+
auditService: options?.auditService
|
|
6151
|
+
});
|
|
5579
6152
|
this.relationService = new RelationService(adapter, registry);
|
|
5580
6153
|
this.userService = new UserService(adapter, tenantId);
|
|
5581
6154
|
this.rollupService = new RollupService(adapter);
|
|
@@ -5730,7 +6303,8 @@ var RecordService = class {
|
|
|
5730
6303
|
objectId,
|
|
5731
6304
|
data,
|
|
5732
6305
|
label,
|
|
5733
|
-
completionStatus
|
|
6306
|
+
completionStatus,
|
|
6307
|
+
metadata: options?.metadata
|
|
5734
6308
|
});
|
|
5735
6309
|
if (!options?.skipHooks) {
|
|
5736
6310
|
const afterCtx = {
|
|
@@ -5844,11 +6418,20 @@ var RecordService = class {
|
|
|
5844
6418
|
}
|
|
5845
6419
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
5846
6420
|
const label = await this.computeLabel(schema, mergedData);
|
|
5847
|
-
const
|
|
6421
|
+
const updatePayload = {
|
|
5848
6422
|
...data,
|
|
5849
6423
|
__completionStatus: completionStatus,
|
|
5850
6424
|
__label: label
|
|
5851
|
-
}
|
|
6425
|
+
};
|
|
6426
|
+
if (options?.metadata !== void 0) {
|
|
6427
|
+
const existingMetadata = existing.metadata ?? {};
|
|
6428
|
+
const mergedMetadata = { ...existingMetadata, ...options.metadata };
|
|
6429
|
+
const cleanedMetadata = Object.fromEntries(
|
|
6430
|
+
Object.entries(mergedMetadata).filter(([, value]) => value !== void 0)
|
|
6431
|
+
);
|
|
6432
|
+
updatePayload.__metadata = cleanedMetadata;
|
|
6433
|
+
}
|
|
6434
|
+
const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
|
|
5852
6435
|
if (!options?.skipHooks) {
|
|
5853
6436
|
const afterCtx = {
|
|
5854
6437
|
...hookCtx,
|
|
@@ -6117,9 +6700,10 @@ var RecordService = class {
|
|
|
6117
6700
|
/**
|
|
6118
6701
|
* Recalculate rollups after a record changes
|
|
6119
6702
|
*
|
|
6120
|
-
* This handles
|
|
6703
|
+
* This handles three cases:
|
|
6121
6704
|
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
6122
|
-
* 2. Parent records have rollups that aggregate from this record
|
|
6705
|
+
* 2. Parent records have rollups that aggregate from this record (reverse pattern)
|
|
6706
|
+
* 3. Records that have forward rollups pointing to this record (forward pattern)
|
|
6123
6707
|
*
|
|
6124
6708
|
* @param record - The record that was modified
|
|
6125
6709
|
* @param schema - Schema of the record's object
|
|
@@ -6133,19 +6717,26 @@ var RecordService = class {
|
|
|
6133
6717
|
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6134
6718
|
}
|
|
6135
6719
|
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
6136
|
-
if (affectedParentIds.length
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
(
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
6720
|
+
if (affectedParentIds.length > 0) {
|
|
6721
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
6722
|
+
for (const parentRecord of parentRecords) {
|
|
6723
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
6724
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
6725
|
+
(a) => a.type === "rollup"
|
|
6726
|
+
);
|
|
6727
|
+
if (rollupAttrs.length > 0) {
|
|
6728
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
6729
|
+
}
|
|
6147
6730
|
}
|
|
6148
6731
|
}
|
|
6732
|
+
const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
|
|
6733
|
+
record,
|
|
6734
|
+
schema
|
|
6735
|
+
);
|
|
6736
|
+
for (const forwardRecord of affectedForwardRecords) {
|
|
6737
|
+
const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
|
|
6738
|
+
await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
6739
|
+
}
|
|
6149
6740
|
}
|
|
6150
6741
|
/**
|
|
6151
6742
|
* Permanently delete a record (hard delete)
|
|
@@ -7261,16 +7852,22 @@ var NoopGeocodingAdapter = class {
|
|
|
7261
7852
|
NoopHookRegistry,
|
|
7262
7853
|
ObjectSchemaService,
|
|
7263
7854
|
PermissionService,
|
|
7855
|
+
QueryBuilder,
|
|
7856
|
+
QueryMultipleResultsError,
|
|
7857
|
+
QueryNoResultError,
|
|
7264
7858
|
RecordService,
|
|
7265
7859
|
RelationResolverService,
|
|
7266
7860
|
RelationService,
|
|
7267
7861
|
RollupScheduler,
|
|
7268
7862
|
RollupService,
|
|
7863
|
+
SHORTCUT_TO_FILTER_OPERATOR,
|
|
7269
7864
|
UserProfileService,
|
|
7270
7865
|
UserService,
|
|
7271
7866
|
ViewService,
|
|
7272
7867
|
buildAuditChanges,
|
|
7868
|
+
createDefaultState,
|
|
7273
7869
|
createMockAdapter,
|
|
7870
|
+
createQueryBuilder,
|
|
7274
7871
|
enrichValuesWithSelectLabels,
|
|
7275
7872
|
evaluateFormula,
|
|
7276
7873
|
evaluateFormulaAttribute,
|
|
@@ -7283,6 +7880,8 @@ var NoopGeocodingAdapter = class {
|
|
|
7283
7880
|
extractRelationReferences,
|
|
7284
7881
|
flattenRelationsForEval,
|
|
7285
7882
|
formatFormulaResult,
|
|
7883
|
+
formatRecord,
|
|
7884
|
+
formatRecords,
|
|
7286
7885
|
getPathDepth,
|
|
7287
7886
|
getRelationPath,
|
|
7288
7887
|
getSyncPreview,
|