@stndrds/schema 0.1.0-alpha.20 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3RVLIIUK.mjs +9257 -0
- package/dist/chunk-6FNFXEQY.mjs +9316 -0
- package/dist/chunk-6TJR4WDK.mjs +9262 -0
- package/dist/chunk-F27AKBTL.mjs +9452 -0
- package/dist/chunk-HSNBVKQU.mjs +9316 -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 +624 -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-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 +612 -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 (replaces existing metadata)
|
|
592
|
+
* const withMeta = await schema.from('products')
|
|
593
|
+
* .eq('id', 'rec-123')
|
|
594
|
+
* .update({ price: 899 }, { metadata: { synced: true } });
|
|
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 {
|
|
@@ -5451,6 +5952,71 @@ var RollupService = class {
|
|
|
5451
5952
|
}
|
|
5452
5953
|
return [...new Set(affectedIds)];
|
|
5453
5954
|
}
|
|
5955
|
+
/**
|
|
5956
|
+
* Find records that have forward rollups pointing to the modified record.
|
|
5957
|
+
*
|
|
5958
|
+
* Forward rollups are rollups where the record has a relation attribute
|
|
5959
|
+
* pointing to another object, and the rollup aggregates values from that target.
|
|
5960
|
+
* When the target record changes, we need to recalculate these rollups.
|
|
5961
|
+
*
|
|
5962
|
+
* Example: Order has relation "company" → Company, and rollup "capitalSocial"
|
|
5963
|
+
* aggregating from the Company. When Company.capitalSocial changes,
|
|
5964
|
+
* all Orders pointing to that Company need their rollup recalculated.
|
|
5965
|
+
*
|
|
5966
|
+
* @param changedRecord - The record that was modified
|
|
5967
|
+
* @param changedSchema - Schema of the changed record's object
|
|
5968
|
+
* @returns Array of records that need their forward rollups recalculated
|
|
5969
|
+
*/
|
|
5970
|
+
async findRecordsWithForwardRollup(changedRecord, changedSchema) {
|
|
5971
|
+
const affectedRecords = [];
|
|
5972
|
+
const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
|
|
5973
|
+
if (!recordObject) {
|
|
5974
|
+
return affectedRecords;
|
|
5975
|
+
}
|
|
5976
|
+
const tenantId = recordObject.tenantId;
|
|
5977
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
5978
|
+
for (const obj of allObjects) {
|
|
5979
|
+
if (obj.id === changedRecord.objectId) {
|
|
5980
|
+
continue;
|
|
5981
|
+
}
|
|
5982
|
+
const attributes = await this.adapter.attributes.findByObjectId(obj.id);
|
|
5983
|
+
const rollupAttrs = attributes.filter((a) => a.type === "rollup");
|
|
5984
|
+
if (rollupAttrs.length === 0) {
|
|
5985
|
+
continue;
|
|
5986
|
+
}
|
|
5987
|
+
for (const rollupDbAttr of rollupAttrs) {
|
|
5988
|
+
const rollupConfig = rollupDbAttr.config;
|
|
5989
|
+
if (!rollupConfig?.relationAttribute) {
|
|
5990
|
+
continue;
|
|
5991
|
+
}
|
|
5992
|
+
const relationAttr = attributes.find(
|
|
5993
|
+
(a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
|
|
5994
|
+
);
|
|
5995
|
+
if (!relationAttr) {
|
|
5996
|
+
continue;
|
|
5997
|
+
}
|
|
5998
|
+
const relationConfig = relationAttr.config;
|
|
5999
|
+
const targetsChangedObject = relationConfig?.targets?.some(
|
|
6000
|
+
(t) => t.object === changedSchema.name
|
|
6001
|
+
);
|
|
6002
|
+
if (!targetsChangedObject) {
|
|
6003
|
+
continue;
|
|
6004
|
+
}
|
|
6005
|
+
const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
|
|
6006
|
+
tenantId,
|
|
6007
|
+
obj.id,
|
|
6008
|
+
relationAttr.name,
|
|
6009
|
+
changedRecord.id
|
|
6010
|
+
);
|
|
6011
|
+
affectedRecords.push(...recordsPointingToChanged);
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
const uniqueRecords = /* @__PURE__ */ new Map();
|
|
6015
|
+
for (const record of affectedRecords) {
|
|
6016
|
+
uniqueRecords.set(record.id, record);
|
|
6017
|
+
}
|
|
6018
|
+
return Array.from(uniqueRecords.values());
|
|
6019
|
+
}
|
|
5454
6020
|
};
|
|
5455
6021
|
|
|
5456
6022
|
// src/runtime/services/user.service.ts
|
|
@@ -5575,7 +6141,11 @@ var RecordService = class {
|
|
|
5575
6141
|
constructor(adapter, tenantId, options) {
|
|
5576
6142
|
this.adapter = adapter;
|
|
5577
6143
|
this.tenantId = tenantId;
|
|
5578
|
-
this.schemaService = new ObjectSchemaService(adapter, registry
|
|
6144
|
+
this.schemaService = new ObjectSchemaService(adapter, registry, {
|
|
6145
|
+
userId: options?.userId,
|
|
6146
|
+
userEmail: options?.userEmail,
|
|
6147
|
+
auditService: options?.auditService
|
|
6148
|
+
});
|
|
5579
6149
|
this.relationService = new RelationService(adapter, registry);
|
|
5580
6150
|
this.userService = new UserService(adapter, tenantId);
|
|
5581
6151
|
this.rollupService = new RollupService(adapter);
|
|
@@ -5730,7 +6300,8 @@ var RecordService = class {
|
|
|
5730
6300
|
objectId,
|
|
5731
6301
|
data,
|
|
5732
6302
|
label,
|
|
5733
|
-
completionStatus
|
|
6303
|
+
completionStatus,
|
|
6304
|
+
metadata: options?.metadata
|
|
5734
6305
|
});
|
|
5735
6306
|
if (!options?.skipHooks) {
|
|
5736
6307
|
const afterCtx = {
|
|
@@ -5844,11 +6415,15 @@ var RecordService = class {
|
|
|
5844
6415
|
}
|
|
5845
6416
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
5846
6417
|
const label = await this.computeLabel(schema, mergedData);
|
|
5847
|
-
const
|
|
6418
|
+
const updatePayload = {
|
|
5848
6419
|
...data,
|
|
5849
6420
|
__completionStatus: completionStatus,
|
|
5850
6421
|
__label: label
|
|
5851
|
-
}
|
|
6422
|
+
};
|
|
6423
|
+
if (options?.metadata !== void 0) {
|
|
6424
|
+
updatePayload.__metadata = options.metadata;
|
|
6425
|
+
}
|
|
6426
|
+
const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
|
|
5852
6427
|
if (!options?.skipHooks) {
|
|
5853
6428
|
const afterCtx = {
|
|
5854
6429
|
...hookCtx,
|
|
@@ -6117,9 +6692,10 @@ var RecordService = class {
|
|
|
6117
6692
|
/**
|
|
6118
6693
|
* Recalculate rollups after a record changes
|
|
6119
6694
|
*
|
|
6120
|
-
* This handles
|
|
6695
|
+
* This handles three cases:
|
|
6121
6696
|
* 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
|
|
6697
|
+
* 2. Parent records have rollups that aggregate from this record (reverse pattern)
|
|
6698
|
+
* 3. Records that have forward rollups pointing to this record (forward pattern)
|
|
6123
6699
|
*
|
|
6124
6700
|
* @param record - The record that was modified
|
|
6125
6701
|
* @param schema - Schema of the record's object
|
|
@@ -6133,19 +6709,26 @@ var RecordService = class {
|
|
|
6133
6709
|
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6134
6710
|
}
|
|
6135
6711
|
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);
|
|
6712
|
+
if (affectedParentIds.length > 0) {
|
|
6713
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
6714
|
+
for (const parentRecord of parentRecords) {
|
|
6715
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
6716
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
6717
|
+
(a) => a.type === "rollup"
|
|
6718
|
+
);
|
|
6719
|
+
if (rollupAttrs.length > 0) {
|
|
6720
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
6721
|
+
}
|
|
6147
6722
|
}
|
|
6148
6723
|
}
|
|
6724
|
+
const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
|
|
6725
|
+
record,
|
|
6726
|
+
schema
|
|
6727
|
+
);
|
|
6728
|
+
for (const forwardRecord of affectedForwardRecords) {
|
|
6729
|
+
const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
|
|
6730
|
+
await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
6731
|
+
}
|
|
6149
6732
|
}
|
|
6150
6733
|
/**
|
|
6151
6734
|
* Permanently delete a record (hard delete)
|
|
@@ -7261,16 +7844,22 @@ var NoopGeocodingAdapter = class {
|
|
|
7261
7844
|
NoopHookRegistry,
|
|
7262
7845
|
ObjectSchemaService,
|
|
7263
7846
|
PermissionService,
|
|
7847
|
+
QueryBuilder,
|
|
7848
|
+
QueryMultipleResultsError,
|
|
7849
|
+
QueryNoResultError,
|
|
7264
7850
|
RecordService,
|
|
7265
7851
|
RelationResolverService,
|
|
7266
7852
|
RelationService,
|
|
7267
7853
|
RollupScheduler,
|
|
7268
7854
|
RollupService,
|
|
7855
|
+
SHORTCUT_TO_FILTER_OPERATOR,
|
|
7269
7856
|
UserProfileService,
|
|
7270
7857
|
UserService,
|
|
7271
7858
|
ViewService,
|
|
7272
7859
|
buildAuditChanges,
|
|
7860
|
+
createDefaultState,
|
|
7273
7861
|
createMockAdapter,
|
|
7862
|
+
createQueryBuilder,
|
|
7274
7863
|
enrichValuesWithSelectLabels,
|
|
7275
7864
|
evaluateFormula,
|
|
7276
7865
|
evaluateFormulaAttribute,
|
|
@@ -7283,6 +7872,8 @@ var NoopGeocodingAdapter = class {
|
|
|
7283
7872
|
extractRelationReferences,
|
|
7284
7873
|
flattenRelationsForEval,
|
|
7285
7874
|
formatFormulaResult,
|
|
7875
|
+
formatRecord,
|
|
7876
|
+
formatRecords,
|
|
7286
7877
|
getPathDepth,
|
|
7287
7878
|
getRelationPath,
|
|
7288
7879
|
getSyncPreview,
|