@stndrds/schema 0.1.0-alpha.16 → 0.1.0-alpha.18
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-2PBQNUTS.mjs +7400 -0
- package/dist/chunk-4D23X5XN.mjs +7257 -0
- package/dist/chunk-4D476S25.mjs +7713 -0
- package/dist/chunk-6RPWHMTH.mjs +7101 -0
- package/dist/chunk-CMO3JEJL.mjs +7259 -0
- package/dist/chunk-CWPOGEK2.mjs +7707 -0
- package/dist/chunk-EWNSOD22.mjs +7707 -0
- package/dist/chunk-JWRGX3UW.mjs +7265 -0
- package/dist/chunk-KZSM5546.mjs +7706 -0
- package/dist/chunk-L7AQ7SZC.mjs +7432 -0
- package/dist/chunk-LZW2MI43.mjs +7404 -0
- package/dist/chunk-OUSLILLE.mjs +7259 -0
- package/dist/chunk-PMJZPJDP.mjs +7265 -0
- package/dist/chunk-V5QXU2OK.mjs +82 -0
- package/dist/chunk-XMGMXAHW.mjs +7706 -0
- package/dist/chunk-YEV46K3M.mjs +7259 -0
- package/dist/default-roles-AKWIWTLE.mjs +20 -0
- package/dist/index.d.mts +55 -5
- package/dist/index.d.ts +55 -5
- package/dist/index.js +890 -110
- package/dist/index.mjs +14 -2
- package/dist/runtime--rp-TG-h.d.mts +5181 -0
- package/dist/runtime--rp-TG-h.d.ts +5181 -0
- package/dist/runtime-DWhSpnn4.d.mts +5256 -0
- package/dist/runtime-DWhSpnn4.d.ts +5256 -0
- package/dist/runtime-Dw2exb5q.d.mts +5601 -0
- package/dist/runtime-Dw2exb5q.d.ts +5601 -0
- package/dist/runtime-DySuNV6Y.d.mts +5245 -0
- package/dist/runtime-DySuNV6Y.d.ts +5245 -0
- package/dist/runtime-DzdsFCyL.d.mts +5601 -0
- package/dist/runtime-DzdsFCyL.d.ts +5601 -0
- package/dist/runtime-uXhTgSIx.d.mts +5260 -0
- package/dist/runtime-uXhTgSIx.d.ts +5260 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +974 -168
- package/dist/runtime.mjs +5 -1
- package/package.json +3 -3
package/dist/runtime.js
CHANGED
|
@@ -59,21 +59,25 @@ var init_default_roles = __esm({
|
|
|
59
59
|
/** Tenant settings and configuration */
|
|
60
60
|
SETTINGS: "settings",
|
|
61
61
|
/** Audit logs and activity trail */
|
|
62
|
-
AUDIT: "audit"
|
|
62
|
+
AUDIT: "audit",
|
|
63
|
+
/** File uploads and storage management */
|
|
64
|
+
FILES: "files"
|
|
63
65
|
};
|
|
64
66
|
ALL_SYSTEM_RESOURCES = [
|
|
65
67
|
SYSTEM_RESOURCES.USERS,
|
|
66
68
|
SYSTEM_RESOURCES.SCHEMA,
|
|
67
69
|
SYSTEM_RESOURCES.ROLES,
|
|
68
70
|
SYSTEM_RESOURCES.SETTINGS,
|
|
69
|
-
SYSTEM_RESOURCES.AUDIT
|
|
71
|
+
SYSTEM_RESOURCES.AUDIT,
|
|
72
|
+
SYSTEM_RESOURCES.FILES
|
|
70
73
|
];
|
|
71
74
|
SYSTEM_RESOURCE_LABELS = {
|
|
72
75
|
users: "Users",
|
|
73
76
|
schema: "Schema",
|
|
74
77
|
roles: "Roles & Permissions",
|
|
75
78
|
settings: "Settings",
|
|
76
|
-
audit: "Audit Logs"
|
|
79
|
+
audit: "Audit Logs",
|
|
80
|
+
files: "Files"
|
|
77
81
|
};
|
|
78
82
|
DEFAULT_ROLES = {
|
|
79
83
|
/** Full platform access - can manage everything */
|
|
@@ -131,9 +135,11 @@ __export(runtime_exports, {
|
|
|
131
135
|
RecordService: () => RecordService,
|
|
132
136
|
RelationService: () => RelationService,
|
|
133
137
|
UserProfileService: () => UserProfileService,
|
|
138
|
+
UserService: () => UserService,
|
|
134
139
|
ViewService: () => ViewService,
|
|
135
140
|
buildAuditChanges: () => buildAuditChanges,
|
|
136
141
|
createMockAdapter: () => createMockAdapter,
|
|
142
|
+
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
137
143
|
extractAttributeNames: () => extractAttributeNames,
|
|
138
144
|
getSyncPreview: () => getSyncPreview,
|
|
139
145
|
getViewSyncPreview: () => getViewSyncPreview,
|
|
@@ -165,6 +171,175 @@ function generateId() {
|
|
|
165
171
|
});
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
// src/format.ts
|
|
175
|
+
var import_constants = require("@stndrds/constants");
|
|
176
|
+
var EMPTY_VALUE_PLACEHOLDER = "\u2014";
|
|
177
|
+
function formatText(value) {
|
|
178
|
+
return String(value);
|
|
179
|
+
}
|
|
180
|
+
function formatCheckbox(value) {
|
|
181
|
+
return value ? "Yes" : "No";
|
|
182
|
+
}
|
|
183
|
+
function formatNumber(value, attribute) {
|
|
184
|
+
if (typeof value !== "number") return String(value);
|
|
185
|
+
const decimals = attribute.decimals;
|
|
186
|
+
if (attribute.unit === "percentage") {
|
|
187
|
+
return value.toLocaleString(void 0, {
|
|
188
|
+
style: "percent",
|
|
189
|
+
minimumFractionDigits: decimals,
|
|
190
|
+
maximumFractionDigits: decimals
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return value.toLocaleString(void 0, {
|
|
194
|
+
minimumFractionDigits: decimals,
|
|
195
|
+
maximumFractionDigits: decimals
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function formatCurrency(value, _attribute) {
|
|
199
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
200
|
+
const currency = value;
|
|
201
|
+
if (!("value" in currency && "code" in currency)) return String(value);
|
|
202
|
+
const formattedValue = currency.value.toLocaleString(void 0, {
|
|
203
|
+
minimumFractionDigits: 2,
|
|
204
|
+
maximumFractionDigits: 2
|
|
205
|
+
});
|
|
206
|
+
return `${formattedValue} ${currency.code}`;
|
|
207
|
+
}
|
|
208
|
+
function formatDate(value) {
|
|
209
|
+
if (value instanceof Date) {
|
|
210
|
+
return value.toISOString().split("T")[0];
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === "string") {
|
|
213
|
+
const date = new Date(value);
|
|
214
|
+
if (!Number.isNaN(date.getTime())) {
|
|
215
|
+
return date.toISOString().split("T")[0];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return String(value);
|
|
219
|
+
}
|
|
220
|
+
function formatTimestamp(value) {
|
|
221
|
+
if (typeof value === "number") {
|
|
222
|
+
return new Date(value).toISOString();
|
|
223
|
+
}
|
|
224
|
+
if (value instanceof Date) {
|
|
225
|
+
return value.toISOString();
|
|
226
|
+
}
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
function formatPhone(value) {
|
|
230
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
231
|
+
const phone = value;
|
|
232
|
+
if (!("phoneNumber" in phone)) return String(value);
|
|
233
|
+
if (phone.countryCode) {
|
|
234
|
+
const country = (0, import_constants.getCountryByIso3)(phone.countryCode);
|
|
235
|
+
const dial = country?.phoneCode ?? "";
|
|
236
|
+
return `${dial} ${phone.phoneNumber}`.trim();
|
|
237
|
+
}
|
|
238
|
+
return phone.phoneNumber;
|
|
239
|
+
}
|
|
240
|
+
function formatLocation(value, attribute) {
|
|
241
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
242
|
+
const loc = value;
|
|
243
|
+
const granularity = attribute.granularity ?? "full";
|
|
244
|
+
const parts = [];
|
|
245
|
+
switch (granularity) {
|
|
246
|
+
case "country":
|
|
247
|
+
if (loc.country) parts.push(loc.country);
|
|
248
|
+
break;
|
|
249
|
+
case "state":
|
|
250
|
+
if (loc.state) parts.push(loc.state);
|
|
251
|
+
if (loc.country) parts.push(loc.country);
|
|
252
|
+
break;
|
|
253
|
+
case "city":
|
|
254
|
+
if (loc.city) parts.push(loc.city);
|
|
255
|
+
if (loc.state) parts.push(loc.state);
|
|
256
|
+
if (loc.country) parts.push(loc.country);
|
|
257
|
+
break;
|
|
258
|
+
case "coordinates":
|
|
259
|
+
if (loc.latitude !== void 0 && loc.longitude !== void 0) {
|
|
260
|
+
parts.push(`${loc.latitude}, ${loc.longitude}`);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case "address":
|
|
264
|
+
if (loc.address) parts.push(loc.address);
|
|
265
|
+
if (loc.city) parts.push(loc.city);
|
|
266
|
+
if (loc.state) parts.push(loc.state);
|
|
267
|
+
if (loc.country) parts.push(loc.country);
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
if (loc.address) parts.push(loc.address);
|
|
271
|
+
if (loc.city) parts.push(loc.city);
|
|
272
|
+
if (loc.state) parts.push(loc.state);
|
|
273
|
+
if (loc.postalCode) parts.push(loc.postalCode);
|
|
274
|
+
if (loc.country) parts.push(loc.country);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
return parts.join(", ") || EMPTY_VALUE_PLACEHOLDER;
|
|
278
|
+
}
|
|
279
|
+
function formatSelect(value, attribute) {
|
|
280
|
+
if (typeof value !== "string") return String(value);
|
|
281
|
+
const option = attribute.options?.find((o) => o.value === value);
|
|
282
|
+
return option?.label ?? String(value);
|
|
283
|
+
}
|
|
284
|
+
function formatMultiselect(value, attribute) {
|
|
285
|
+
if (!Array.isArray(value)) return String(value);
|
|
286
|
+
if (attribute.options) {
|
|
287
|
+
const labels = value.map((v) => attribute.options.find((o) => o.value === v)?.label).filter(Boolean);
|
|
288
|
+
return labels.join(", ");
|
|
289
|
+
}
|
|
290
|
+
return value.join(", ");
|
|
291
|
+
}
|
|
292
|
+
function formatRating(value, attribute) {
|
|
293
|
+
if (typeof value !== "number") return String(value);
|
|
294
|
+
const max = attribute.max ?? 5;
|
|
295
|
+
return `${value}/${max}`;
|
|
296
|
+
}
|
|
297
|
+
function formatAttributeValue(value, attribute) {
|
|
298
|
+
if (value === null || value === void 0 || value === "") {
|
|
299
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
300
|
+
}
|
|
301
|
+
switch (attribute.type) {
|
|
302
|
+
case "text":
|
|
303
|
+
case "textarea":
|
|
304
|
+
return formatText(value);
|
|
305
|
+
case "checkbox":
|
|
306
|
+
return formatCheckbox(value);
|
|
307
|
+
case "number":
|
|
308
|
+
return formatNumber(value, attribute);
|
|
309
|
+
case "currency":
|
|
310
|
+
return formatCurrency(value, attribute);
|
|
311
|
+
case "date":
|
|
312
|
+
return formatDate(value);
|
|
313
|
+
case "timestamp":
|
|
314
|
+
return formatTimestamp(value);
|
|
315
|
+
case "phone":
|
|
316
|
+
return formatPhone(value);
|
|
317
|
+
case "location":
|
|
318
|
+
return formatLocation(value, attribute);
|
|
319
|
+
case "select":
|
|
320
|
+
case "status":
|
|
321
|
+
return formatSelect(value, attribute);
|
|
322
|
+
case "multiselect":
|
|
323
|
+
return formatMultiselect(value, attribute);
|
|
324
|
+
case "rating":
|
|
325
|
+
return formatRating(value, attribute);
|
|
326
|
+
// Unsupported types - return value as-is or placeholder
|
|
327
|
+
case "file":
|
|
328
|
+
case "user":
|
|
329
|
+
case "relation":
|
|
330
|
+
if (Array.isArray(value)) {
|
|
331
|
+
return value.join(", ");
|
|
332
|
+
}
|
|
333
|
+
return String(value);
|
|
334
|
+
default: {
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
return value.join(", ");
|
|
337
|
+
}
|
|
338
|
+
return String(value);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
168
343
|
// src/runtime/template.ts
|
|
169
344
|
var pipes = {
|
|
170
345
|
/** Convert to uppercase */
|
|
@@ -216,6 +391,24 @@ function extractAttributeNames(template) {
|
|
|
216
391
|
}
|
|
217
392
|
return names;
|
|
218
393
|
}
|
|
394
|
+
function hasOptions(attr) {
|
|
395
|
+
return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
|
|
396
|
+
}
|
|
397
|
+
function enrichValuesWithSelectLabels(values, attributes) {
|
|
398
|
+
const enriched = { ...values };
|
|
399
|
+
for (const attr of attributes) {
|
|
400
|
+
const value = values[attr.name];
|
|
401
|
+
if (value == null) continue;
|
|
402
|
+
const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
|
|
403
|
+
if (!(isSelectLike && hasOptions(attr))) continue;
|
|
404
|
+
if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
|
|
405
|
+
const formatted = formatAttributeValue(value, attr);
|
|
406
|
+
if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
|
|
407
|
+
enriched[attr.name] = formatted;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return enriched;
|
|
411
|
+
}
|
|
219
412
|
|
|
220
413
|
// src/runtime/mock-adapter.ts
|
|
221
414
|
function createMockObjectsRepository(stores) {
|
|
@@ -692,14 +885,31 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
692
885
|
(options.offset ?? 0) + options.limit
|
|
693
886
|
);
|
|
694
887
|
}
|
|
888
|
+
const attributesByObjectId = /* @__PURE__ */ new Map();
|
|
889
|
+
for (const attr of stores.attributes.values()) {
|
|
890
|
+
if (!attributesByObjectId.has(attr.objectId)) {
|
|
891
|
+
attributesByObjectId.set(attr.objectId, []);
|
|
892
|
+
}
|
|
893
|
+
attributesByObjectId.get(attr.objectId)?.push(attr);
|
|
894
|
+
}
|
|
695
895
|
const results = matchingRecords.map((r) => {
|
|
696
896
|
const obj = objectsMap.get(r.objectId);
|
|
697
897
|
const labelExpression = obj?.labelExpression ?? "{{ name }}";
|
|
898
|
+
const dbAttrs = attributesByObjectId.get(r.objectId) ?? [];
|
|
899
|
+
const attrs = dbAttrs.map((a) => ({
|
|
900
|
+
...a.config,
|
|
901
|
+
id: a.id,
|
|
902
|
+
name: a.name,
|
|
903
|
+
type: a.type,
|
|
904
|
+
label: a.config.label ?? a.name,
|
|
905
|
+
required: a.config.required ?? false
|
|
906
|
+
}));
|
|
907
|
+
const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
|
|
698
908
|
return {
|
|
699
909
|
objectId: r.objectId,
|
|
700
910
|
objectName: obj?.name ?? "unknown",
|
|
701
911
|
objectLabel: obj?.label ?? "Unknown",
|
|
702
|
-
label: renderLabelExpression(labelExpression,
|
|
912
|
+
label: renderLabelExpression(labelExpression, enrichedValues),
|
|
703
913
|
recordId: r.id,
|
|
704
914
|
values: r.values,
|
|
705
915
|
completionStatus: r.completionStatus,
|
|
@@ -1116,6 +1326,10 @@ var AuditService = class {
|
|
|
1116
1326
|
this.options = options;
|
|
1117
1327
|
this.buffer = [];
|
|
1118
1328
|
this.flushTimer = null;
|
|
1329
|
+
/** Prevents concurrent flush operations */
|
|
1330
|
+
this.isFlushing = false;
|
|
1331
|
+
/** Pending flush promise to allow waiting on concurrent flush */
|
|
1332
|
+
this.flushPromise = null;
|
|
1119
1333
|
if (options?.async && options.flushIntervalMs) {
|
|
1120
1334
|
this.startFlushTimer();
|
|
1121
1335
|
}
|
|
@@ -1202,6 +1416,24 @@ var AuditService = class {
|
|
|
1202
1416
|
};
|
|
1203
1417
|
await this.log(entry);
|
|
1204
1418
|
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Log a file action (upload, update, delete)
|
|
1421
|
+
*/
|
|
1422
|
+
async logFileAction(params) {
|
|
1423
|
+
const entry = {
|
|
1424
|
+
tenantId: this.tenantId,
|
|
1425
|
+
actorId: params.actorId,
|
|
1426
|
+
actorEmail: params.actorEmail,
|
|
1427
|
+
actorType: "user",
|
|
1428
|
+
action: params.action,
|
|
1429
|
+
resourceType: "file",
|
|
1430
|
+
resourceId: params.fileId,
|
|
1431
|
+
resourceLabel: params.fileName,
|
|
1432
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
1433
|
+
metadata: params.metadata
|
|
1434
|
+
};
|
|
1435
|
+
await this.log(entry);
|
|
1436
|
+
}
|
|
1205
1437
|
/**
|
|
1206
1438
|
* Log a system action (no actor)
|
|
1207
1439
|
*/
|
|
@@ -1268,14 +1500,23 @@ var AuditService = class {
|
|
|
1268
1500
|
// ============================================================================
|
|
1269
1501
|
/**
|
|
1270
1502
|
* Flush buffered logs to the database
|
|
1503
|
+
* Protected against concurrent flush calls
|
|
1271
1504
|
*/
|
|
1272
1505
|
async flush() {
|
|
1506
|
+
if (this.isFlushing && this.flushPromise) {
|
|
1507
|
+
return this.flushPromise;
|
|
1508
|
+
}
|
|
1273
1509
|
if (this.buffer.length === 0 || !this.adapter.audit) {
|
|
1274
1510
|
return;
|
|
1275
1511
|
}
|
|
1512
|
+
this.isFlushing = true;
|
|
1276
1513
|
const entries = [...this.buffer];
|
|
1277
1514
|
this.buffer = [];
|
|
1278
|
-
|
|
1515
|
+
this.flushPromise = this.adapter.audit.createMany(entries).finally(() => {
|
|
1516
|
+
this.isFlushing = false;
|
|
1517
|
+
this.flushPromise = null;
|
|
1518
|
+
});
|
|
1519
|
+
return this.flushPromise;
|
|
1279
1520
|
}
|
|
1280
1521
|
/**
|
|
1281
1522
|
* Clean up resources (stop timer, flush remaining logs)
|
|
@@ -1297,6 +1538,12 @@ var AuditService = class {
|
|
|
1297
1538
|
if (!this.adapter.audit) {
|
|
1298
1539
|
return;
|
|
1299
1540
|
}
|
|
1541
|
+
if (entry.actorId && !entry.actorEmail && entry.actorType === "user") {
|
|
1542
|
+
const profile = await this.adapter.userProfiles.findById(entry.actorId);
|
|
1543
|
+
if (profile) {
|
|
1544
|
+
entry.actorEmail = profile.email;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1300
1547
|
if (this.options?.async) {
|
|
1301
1548
|
this.buffer.push(entry);
|
|
1302
1549
|
const batchSize = this.options.batchSize ?? 10;
|
|
@@ -1336,21 +1583,105 @@ var AuditService = class {
|
|
|
1336
1583
|
|
|
1337
1584
|
// src/runtime/services/file.service.ts
|
|
1338
1585
|
var FileService = class {
|
|
1339
|
-
constructor(adapter, tenantId) {
|
|
1586
|
+
constructor(adapter, tenantId, options) {
|
|
1340
1587
|
this.adapter = adapter;
|
|
1341
1588
|
this.tenantId = tenantId;
|
|
1589
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
1590
|
+
this.userId = options?.userId;
|
|
1591
|
+
this.userEmail = options?.userEmail;
|
|
1342
1592
|
}
|
|
1593
|
+
// ============================================================================
|
|
1594
|
+
// UPLOAD (requires StorageAdapter)
|
|
1595
|
+
// ============================================================================
|
|
1343
1596
|
/**
|
|
1344
|
-
*
|
|
1597
|
+
* Upload a file to storage and create metadata record.
|
|
1345
1598
|
*
|
|
1346
|
-
*
|
|
1599
|
+
* This method orchestrates:
|
|
1600
|
+
* 1. Upload to storage (via StorageAdapter)
|
|
1601
|
+
* 2. Create file metadata in database
|
|
1602
|
+
* 3. Audit log the operation
|
|
1603
|
+
*
|
|
1604
|
+
* Requires `adapter.storage` to be configured.
|
|
1605
|
+
*
|
|
1606
|
+
* @param input - File content and metadata
|
|
1347
1607
|
* @returns Created file record
|
|
1608
|
+
* @throws Error if StorageAdapter is not configured
|
|
1348
1609
|
*
|
|
1349
1610
|
* @example
|
|
1350
1611
|
* ```typescript
|
|
1351
|
-
* const
|
|
1612
|
+
* const file = await service.uploadFile({
|
|
1613
|
+
* content: fileBuffer,
|
|
1614
|
+
* fileName: "document.pdf",
|
|
1615
|
+
* mimeType: "application/pdf",
|
|
1616
|
+
* size: 12345,
|
|
1617
|
+
* uploadedBy: "user-123",
|
|
1618
|
+
* visibility: "private",
|
|
1619
|
+
* folderPath: "/documents",
|
|
1620
|
+
* tags: ["contract", "2025"],
|
|
1621
|
+
* });
|
|
1622
|
+
* ```
|
|
1623
|
+
*/
|
|
1624
|
+
async uploadFile(input) {
|
|
1625
|
+
if (!this.adapter.storage) {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
"StorageAdapter is not configured. Provide adapter.storage to use uploadFile()."
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
const uploadResult = await this.adapter.storage.upload({
|
|
1631
|
+
content: input.content,
|
|
1632
|
+
fileName: input.fileName,
|
|
1633
|
+
mimeType: input.mimeType,
|
|
1634
|
+
size: input.size,
|
|
1635
|
+
tenantId: this.tenantId,
|
|
1636
|
+
folderPath: input.folderPath
|
|
1637
|
+
});
|
|
1638
|
+
const file = await this.adapter.files.create({
|
|
1639
|
+
tenantId: this.tenantId,
|
|
1640
|
+
name: input.fileName,
|
|
1641
|
+
originalName: input.fileName,
|
|
1642
|
+
mimeType: input.mimeType,
|
|
1643
|
+
size: input.size,
|
|
1644
|
+
storageProvider: uploadResult.storageProvider,
|
|
1645
|
+
storagePath: uploadResult.storagePath,
|
|
1646
|
+
storageBucket: uploadResult.storageBucket,
|
|
1647
|
+
url: uploadResult.url,
|
|
1648
|
+
uploadedBy: input.uploadedBy,
|
|
1649
|
+
folderPath: input.folderPath,
|
|
1650
|
+
tags: input.tags,
|
|
1651
|
+
visibility: input.visibility ?? "private",
|
|
1652
|
+
allowedUsers: input.allowedUsers
|
|
1653
|
+
});
|
|
1654
|
+
if (this.auditService && this.userId) {
|
|
1655
|
+
await this.auditService.logFileAction({
|
|
1656
|
+
action: "file.uploaded",
|
|
1657
|
+
actorId: this.userId,
|
|
1658
|
+
actorEmail: this.userEmail,
|
|
1659
|
+
fileId: file.id,
|
|
1660
|
+
fileName: file.name,
|
|
1661
|
+
metadata: {
|
|
1662
|
+
mimeType: file.mimeType,
|
|
1663
|
+
size: file.size,
|
|
1664
|
+
visibility: file.visibility
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
return file;
|
|
1669
|
+
}
|
|
1670
|
+
// ============================================================================
|
|
1671
|
+
// CREATE (metadata only, for external storage)
|
|
1672
|
+
// ============================================================================
|
|
1673
|
+
/**
|
|
1674
|
+
* Create a new file record (after upload to storage).
|
|
1675
|
+
*
|
|
1676
|
+
* Use this method when handling storage externally (e.g., with Multer + S3).
|
|
1677
|
+
* For integrated upload, use `uploadFile()` instead.
|
|
1678
|
+
*
|
|
1679
|
+
* @param data - File metadata
|
|
1680
|
+
* @returns Created file record
|
|
1352
1681
|
*
|
|
1353
|
-
*
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```typescript
|
|
1684
|
+
* // After uploading to S3 with Multer
|
|
1354
1685
|
* const file = await service.createFile({
|
|
1355
1686
|
* tenantId: "tenant-123",
|
|
1356
1687
|
* name: "contract-2025.pdf",
|
|
@@ -1372,8 +1703,26 @@ var FileService = class {
|
|
|
1372
1703
|
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
1373
1704
|
);
|
|
1374
1705
|
}
|
|
1375
|
-
|
|
1706
|
+
const file = await this.adapter.files.create(data);
|
|
1707
|
+
if (this.auditService && this.userId) {
|
|
1708
|
+
await this.auditService.logFileAction({
|
|
1709
|
+
action: "file.uploaded",
|
|
1710
|
+
actorId: this.userId,
|
|
1711
|
+
actorEmail: this.userEmail,
|
|
1712
|
+
fileId: file.id,
|
|
1713
|
+
fileName: file.name,
|
|
1714
|
+
metadata: {
|
|
1715
|
+
mimeType: file.mimeType,
|
|
1716
|
+
size: file.size,
|
|
1717
|
+
visibility: file.visibility
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
return file;
|
|
1376
1722
|
}
|
|
1723
|
+
// ============================================================================
|
|
1724
|
+
// READ
|
|
1725
|
+
// ============================================================================
|
|
1377
1726
|
/**
|
|
1378
1727
|
* Get file by ID
|
|
1379
1728
|
*/
|
|
@@ -1394,6 +1743,9 @@ var FileService = class {
|
|
|
1394
1743
|
}
|
|
1395
1744
|
return file;
|
|
1396
1745
|
}
|
|
1746
|
+
// ============================================================================
|
|
1747
|
+
// UPDATE
|
|
1748
|
+
// ============================================================================
|
|
1397
1749
|
/**
|
|
1398
1750
|
* Update file metadata
|
|
1399
1751
|
*
|
|
@@ -1402,18 +1754,44 @@ var FileService = class {
|
|
|
1402
1754
|
* @returns Updated file
|
|
1403
1755
|
*/
|
|
1404
1756
|
async updateFile(fileId, data) {
|
|
1405
|
-
await this.getFileOrThrow(fileId);
|
|
1406
|
-
|
|
1757
|
+
const existingFile = await this.getFileOrThrow(fileId);
|
|
1758
|
+
const updatedFile = await this.adapter.files.update(fileId, data);
|
|
1759
|
+
if (this.auditService && this.userId) {
|
|
1760
|
+
const changes = [];
|
|
1761
|
+
for (const key of Object.keys(data)) {
|
|
1762
|
+
if (data[key] !== void 0 && data[key] !== existingFile[key]) {
|
|
1763
|
+
changes.push({
|
|
1764
|
+
field: key,
|
|
1765
|
+
oldValue: existingFile[key],
|
|
1766
|
+
newValue: data[key]
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (changes.length > 0) {
|
|
1771
|
+
await this.auditService.logFileAction({
|
|
1772
|
+
action: "file.updated",
|
|
1773
|
+
actorId: this.userId,
|
|
1774
|
+
actorEmail: this.userEmail,
|
|
1775
|
+
fileId,
|
|
1776
|
+
fileName: updatedFile.name,
|
|
1777
|
+
changes
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return updatedFile;
|
|
1407
1782
|
}
|
|
1783
|
+
// ============================================================================
|
|
1784
|
+
// DELETE
|
|
1785
|
+
// ============================================================================
|
|
1408
1786
|
/**
|
|
1409
|
-
* Delete file (soft delete)
|
|
1787
|
+
* Delete file (soft delete by default)
|
|
1410
1788
|
*
|
|
1411
1789
|
* @param fileId - File UUID
|
|
1412
1790
|
* @param options - Delete options
|
|
1413
1791
|
*/
|
|
1414
1792
|
async deleteFile(fileId, options) {
|
|
1793
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1415
1794
|
if (options?.checkOwnership && options.userId) {
|
|
1416
|
-
const file = await this.getFileOrThrow(fileId);
|
|
1417
1795
|
if (file.uploadedBy !== options.userId) {
|
|
1418
1796
|
throw new Error("You can only delete files you uploaded");
|
|
1419
1797
|
}
|
|
@@ -1423,7 +1801,84 @@ var FileService = class {
|
|
|
1423
1801
|
} else {
|
|
1424
1802
|
await this.adapter.files.delete(fileId);
|
|
1425
1803
|
}
|
|
1804
|
+
if (this.auditService && this.userId) {
|
|
1805
|
+
await this.auditService.logFileAction({
|
|
1806
|
+
action: "file.deleted",
|
|
1807
|
+
actorId: this.userId,
|
|
1808
|
+
actorEmail: this.userEmail,
|
|
1809
|
+
fileId,
|
|
1810
|
+
fileName: file.name
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Delete file from both storage and database.
|
|
1816
|
+
*
|
|
1817
|
+
* Requires `adapter.storage` to be configured.
|
|
1818
|
+
*
|
|
1819
|
+
* @param fileId - File UUID
|
|
1820
|
+
* @param options - Delete options
|
|
1821
|
+
* @throws Error if StorageAdapter is not configured
|
|
1822
|
+
*/
|
|
1823
|
+
async deleteFileWithStorage(fileId, options) {
|
|
1824
|
+
if (!this.adapter.storage) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
"StorageAdapter is not configured. Provide adapter.storage to use deleteFileWithStorage()."
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1830
|
+
await this.adapter.storage.delete(file.storagePath);
|
|
1831
|
+
if (options?.hard) {
|
|
1832
|
+
await this.adapter.files.hardDelete(fileId);
|
|
1833
|
+
} else {
|
|
1834
|
+
await this.adapter.files.delete(fileId);
|
|
1835
|
+
}
|
|
1836
|
+
if (this.auditService && this.userId) {
|
|
1837
|
+
await this.auditService.logFileAction({
|
|
1838
|
+
action: "file.deleted",
|
|
1839
|
+
actorId: this.userId,
|
|
1840
|
+
actorEmail: this.userEmail,
|
|
1841
|
+
fileId,
|
|
1842
|
+
fileName: file.name,
|
|
1843
|
+
metadata: { deletedFromStorage: true }
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Delete multiple files
|
|
1849
|
+
*
|
|
1850
|
+
* @param fileIds - Array of file UUIDs
|
|
1851
|
+
* @param options - Delete options
|
|
1852
|
+
*/
|
|
1853
|
+
async bulkDelete(fileIds, options) {
|
|
1854
|
+
for (const fileId of fileIds) {
|
|
1855
|
+
const file = await this.getFile(fileId);
|
|
1856
|
+
if (!file) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (options?.deleteFromStorage && this.adapter.storage) {
|
|
1860
|
+
await this.adapter.storage.delete(file.storagePath);
|
|
1861
|
+
}
|
|
1862
|
+
if (options?.hard) {
|
|
1863
|
+
await this.adapter.files.hardDelete(fileId);
|
|
1864
|
+
} else {
|
|
1865
|
+
await this.adapter.files.delete(fileId);
|
|
1866
|
+
}
|
|
1867
|
+
if (this.auditService && this.userId) {
|
|
1868
|
+
await this.auditService.logFileAction({
|
|
1869
|
+
action: "file.deleted",
|
|
1870
|
+
actorId: this.userId,
|
|
1871
|
+
actorEmail: this.userEmail,
|
|
1872
|
+
fileId,
|
|
1873
|
+
fileName: file.name,
|
|
1874
|
+
metadata: { deletedFromStorage: options?.deleteFromStorage ?? false }
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1426
1878
|
}
|
|
1879
|
+
// ============================================================================
|
|
1880
|
+
// LIST
|
|
1881
|
+
// ============================================================================
|
|
1427
1882
|
/**
|
|
1428
1883
|
* List files for the tenant
|
|
1429
1884
|
*/
|
|
@@ -1442,6 +1897,74 @@ var FileService = class {
|
|
|
1442
1897
|
async listFilesByUploader(uploadedBy) {
|
|
1443
1898
|
return await this.adapter.files.findByUploader(uploadedBy);
|
|
1444
1899
|
}
|
|
1900
|
+
// ============================================================================
|
|
1901
|
+
// SIGNED URL
|
|
1902
|
+
// ============================================================================
|
|
1903
|
+
/**
|
|
1904
|
+
* Get a signed URL for private file access.
|
|
1905
|
+
*
|
|
1906
|
+
* Checks access permissions before generating URL.
|
|
1907
|
+
* Requires `adapter.storage` to be configured.
|
|
1908
|
+
*
|
|
1909
|
+
* @param fileId - File UUID
|
|
1910
|
+
* @param userId - User requesting access
|
|
1911
|
+
* @param options - Signed URL options
|
|
1912
|
+
* @returns Signed URL
|
|
1913
|
+
* @throws Error if user doesn't have access or StorageAdapter is not configured
|
|
1914
|
+
*
|
|
1915
|
+
* @example
|
|
1916
|
+
* ```typescript
|
|
1917
|
+
* const url = await service.getSignedUrl("file-123", "user-456", {
|
|
1918
|
+
* expiresIn: 3600, // 1 hour
|
|
1919
|
+
* });
|
|
1920
|
+
* ```
|
|
1921
|
+
*/
|
|
1922
|
+
async getSignedUrl(fileId, userId, options) {
|
|
1923
|
+
const file = await this.getFileOrThrow(fileId);
|
|
1924
|
+
const hasAccess = await this.checkAccess(fileId, userId);
|
|
1925
|
+
if (!hasAccess) {
|
|
1926
|
+
throw new Error("Access denied to this file");
|
|
1927
|
+
}
|
|
1928
|
+
if (file.visibility === "public") {
|
|
1929
|
+
return file.url;
|
|
1930
|
+
}
|
|
1931
|
+
if (!this.adapter.storage) {
|
|
1932
|
+
return file.url;
|
|
1933
|
+
}
|
|
1934
|
+
return await this.adapter.storage.getSignedUrl(file.storagePath, options);
|
|
1935
|
+
}
|
|
1936
|
+
// ============================================================================
|
|
1937
|
+
// ACCESS CONTROL
|
|
1938
|
+
// ============================================================================
|
|
1939
|
+
/**
|
|
1940
|
+
* Check if user has access to a file
|
|
1941
|
+
*
|
|
1942
|
+
* @param fileId - File UUID
|
|
1943
|
+
* @param userId - User ID to check
|
|
1944
|
+
* @returns true if user can access the file
|
|
1945
|
+
*/
|
|
1946
|
+
async checkAccess(fileId, userId) {
|
|
1947
|
+
const file = await this.getFile(fileId);
|
|
1948
|
+
if (!file) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
if (file.visibility === "public") {
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
if (file.uploadedBy === userId) {
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
if (file.visibility === "restricted") {
|
|
1958
|
+
return file.allowedUsers?.includes(userId) ?? false;
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* @deprecated Use checkAccess() instead
|
|
1964
|
+
*/
|
|
1965
|
+
canAccess(fileId, userId) {
|
|
1966
|
+
return this.checkAccess(fileId, userId);
|
|
1967
|
+
}
|
|
1445
1968
|
/**
|
|
1446
1969
|
* Change file visibility
|
|
1447
1970
|
*
|
|
@@ -1485,29 +2008,9 @@ var FileService = class {
|
|
|
1485
2008
|
allowedUsers: newAllowed
|
|
1486
2009
|
});
|
|
1487
2010
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
* @param fileId - File UUID
|
|
1492
|
-
* @param userId - User ID to check
|
|
1493
|
-
* @returns true if user can access the file
|
|
1494
|
-
*/
|
|
1495
|
-
async canAccess(fileId, userId) {
|
|
1496
|
-
const file = await this.getFile(fileId);
|
|
1497
|
-
if (!file) {
|
|
1498
|
-
return false;
|
|
1499
|
-
}
|
|
1500
|
-
if (file.visibility === "public") {
|
|
1501
|
-
return true;
|
|
1502
|
-
}
|
|
1503
|
-
if (file.uploadedBy === userId) {
|
|
1504
|
-
return true;
|
|
1505
|
-
}
|
|
1506
|
-
if (file.visibility === "restricted") {
|
|
1507
|
-
return file.allowedUsers?.includes(userId) ?? false;
|
|
1508
|
-
}
|
|
1509
|
-
return false;
|
|
1510
|
-
}
|
|
2011
|
+
// ============================================================================
|
|
2012
|
+
// ORGANIZATION
|
|
2013
|
+
// ============================================================================
|
|
1511
2014
|
/**
|
|
1512
2015
|
* Move file to different folder
|
|
1513
2016
|
*/
|
|
@@ -1534,6 +2037,115 @@ var FileService = class {
|
|
|
1534
2037
|
}
|
|
1535
2038
|
};
|
|
1536
2039
|
|
|
2040
|
+
// src/exceptions.ts
|
|
2041
|
+
var SchemaErrorCode = {
|
|
2042
|
+
// Generic
|
|
2043
|
+
UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
|
|
2044
|
+
// Not Found
|
|
2045
|
+
OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
|
|
2046
|
+
ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
|
|
2047
|
+
RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
|
|
2048
|
+
USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
|
|
2049
|
+
FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
|
|
2050
|
+
ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
|
|
2051
|
+
// Validation
|
|
2052
|
+
VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
2053
|
+
INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
|
|
2054
|
+
INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
|
|
2055
|
+
// Protected Resources
|
|
2056
|
+
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
2057
|
+
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
2058
|
+
PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
|
|
2059
|
+
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
2060
|
+
// Permissions
|
|
2061
|
+
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
2062
|
+
// Sync
|
|
2063
|
+
SYNC_FAILED: "SCHEMA_SYNC_FAILED",
|
|
2064
|
+
NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
|
|
2065
|
+
// Duplicates
|
|
2066
|
+
DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
|
|
2067
|
+
DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
|
|
2068
|
+
};
|
|
2069
|
+
var SchemaError = class extends Error {
|
|
2070
|
+
constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
|
|
2071
|
+
super(message);
|
|
2072
|
+
this.name = "SchemaError";
|
|
2073
|
+
this.code = code;
|
|
2074
|
+
this.details = details;
|
|
2075
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
2076
|
+
}
|
|
2077
|
+
toJSON() {
|
|
2078
|
+
return {
|
|
2079
|
+
name: this.name,
|
|
2080
|
+
code: this.code,
|
|
2081
|
+
message: this.message,
|
|
2082
|
+
details: this.details
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
var NotFoundError = class extends SchemaError {
|
|
2087
|
+
constructor(resourceType, resourceId, code = SchemaErrorCode.RECORD_NOT_FOUND) {
|
|
2088
|
+
super(`${resourceType} with id "${resourceId}" not found`, code, {
|
|
2089
|
+
resourceType,
|
|
2090
|
+
resourceId
|
|
2091
|
+
});
|
|
2092
|
+
this.name = "NotFoundError";
|
|
2093
|
+
this.resourceType = resourceType;
|
|
2094
|
+
this.resourceId = resourceId;
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
var RecordNotFoundError = class extends NotFoundError {
|
|
2098
|
+
constructor(recordId) {
|
|
2099
|
+
super("Record", recordId, SchemaErrorCode.RECORD_NOT_FOUND);
|
|
2100
|
+
this.name = "RecordNotFoundError";
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
var ValidationError = class _ValidationError extends SchemaError {
|
|
2104
|
+
constructor(message, errors) {
|
|
2105
|
+
super(message, SchemaErrorCode.VALIDATION_FAILED, { errors });
|
|
2106
|
+
this.name = "ValidationError";
|
|
2107
|
+
this.errors = errors;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Create a validation error from Zod-style errors
|
|
2111
|
+
*/
|
|
2112
|
+
static fromZodErrors(errors) {
|
|
2113
|
+
const details = errors.map((err) => ({
|
|
2114
|
+
path: err.path.map(String),
|
|
2115
|
+
message: err.message
|
|
2116
|
+
}));
|
|
2117
|
+
const message = `Validation failed: ${details.map((d) => `${d.path.join(".")}: ${d.message}`).join(", ")}`;
|
|
2118
|
+
return new _ValidationError(message, details);
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
var ProtectedResourceError = class extends SchemaError {
|
|
2122
|
+
constructor(resourceType, resourceName, operation) {
|
|
2123
|
+
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
2124
|
+
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
2125
|
+
resourceType,
|
|
2126
|
+
resourceName,
|
|
2127
|
+
operation
|
|
2128
|
+
});
|
|
2129
|
+
this.name = "ProtectedResourceError";
|
|
2130
|
+
this.resourceType = resourceType;
|
|
2131
|
+
this.resourceName = resourceName;
|
|
2132
|
+
this.operation = operation;
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
var ForbiddenError = class extends SchemaError {
|
|
2136
|
+
constructor(objectName, action, userId) {
|
|
2137
|
+
super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
|
|
2138
|
+
objectName,
|
|
2139
|
+
action,
|
|
2140
|
+
userId
|
|
2141
|
+
});
|
|
2142
|
+
this.name = "ForbiddenError";
|
|
2143
|
+
this.objectName = objectName;
|
|
2144
|
+
this.action = action;
|
|
2145
|
+
this.userId = userId;
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
|
|
1537
2149
|
// src/runtime/services/flow.service.ts
|
|
1538
2150
|
var FlowService = class {
|
|
1539
2151
|
constructor(adapter, systemFlows = []) {
|
|
@@ -1594,16 +2206,26 @@ var FlowService = class {
|
|
|
1594
2206
|
*/
|
|
1595
2207
|
async createFlow(input, tenantId) {
|
|
1596
2208
|
if (!this.adapter.flows) {
|
|
1597
|
-
throw new
|
|
2209
|
+
throw new SchemaError(
|
|
2210
|
+
"Flows feature is not enabled. Database adapter does not support flows.",
|
|
2211
|
+
SchemaErrorCode.UNKNOWN,
|
|
2212
|
+
{ feature: "flows" }
|
|
2213
|
+
);
|
|
1598
2214
|
}
|
|
1599
2215
|
this.validateFlowName(input.name);
|
|
1600
2216
|
const existing = await this.adapter.flows.findByName(tenantId, input.name);
|
|
1601
2217
|
if (existing) {
|
|
1602
|
-
throw new
|
|
2218
|
+
throw new SchemaError(
|
|
2219
|
+
`Flow "${input.name}" already exists`,
|
|
2220
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
2221
|
+
{ flowName: input.name }
|
|
2222
|
+
);
|
|
1603
2223
|
}
|
|
1604
2224
|
if (this.systemFlows.has(input.name)) {
|
|
1605
|
-
throw new
|
|
1606
|
-
`Cannot create flow "${input.name}": a system flow with this name already exists
|
|
2225
|
+
throw new SchemaError(
|
|
2226
|
+
`Cannot create flow "${input.name}": a system flow with this name already exists`,
|
|
2227
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
2228
|
+
{ flowName: input.name, system: true }
|
|
1607
2229
|
);
|
|
1608
2230
|
}
|
|
1609
2231
|
this.validateFlowStructure(input);
|
|
@@ -1628,14 +2250,16 @@ var FlowService = class {
|
|
|
1628
2250
|
*/
|
|
1629
2251
|
async updateFlow(flowId, input) {
|
|
1630
2252
|
if (!this.adapter.flows) {
|
|
1631
|
-
throw new
|
|
2253
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2254
|
+
feature: "flows"
|
|
2255
|
+
});
|
|
1632
2256
|
}
|
|
1633
2257
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1634
2258
|
if (!dbFlow) {
|
|
1635
|
-
throw new
|
|
2259
|
+
throw new NotFoundError("Flow", flowId);
|
|
1636
2260
|
}
|
|
1637
2261
|
if (dbFlow.system) {
|
|
1638
|
-
throw new
|
|
2262
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1639
2263
|
}
|
|
1640
2264
|
if (input.slots || input.pages || input.relations) {
|
|
1641
2265
|
this.validateFlowStructure({
|
|
@@ -1662,14 +2286,16 @@ var FlowService = class {
|
|
|
1662
2286
|
*/
|
|
1663
2287
|
async publishFlow(flowId) {
|
|
1664
2288
|
if (!this.adapter.flows) {
|
|
1665
|
-
throw new
|
|
2289
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2290
|
+
feature: "flows"
|
|
2291
|
+
});
|
|
1666
2292
|
}
|
|
1667
2293
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1668
2294
|
if (!dbFlow) {
|
|
1669
|
-
throw new
|
|
2295
|
+
throw new NotFoundError("Flow", flowId);
|
|
1670
2296
|
}
|
|
1671
2297
|
if (dbFlow.system) {
|
|
1672
|
-
throw new
|
|
2298
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1673
2299
|
}
|
|
1674
2300
|
if (dbFlow.status === "published") {
|
|
1675
2301
|
return this.convertDBFlowToDefinition(dbFlow);
|
|
@@ -1692,14 +2318,16 @@ var FlowService = class {
|
|
|
1692
2318
|
*/
|
|
1693
2319
|
async archiveFlow(flowId) {
|
|
1694
2320
|
if (!this.adapter.flows) {
|
|
1695
|
-
throw new
|
|
2321
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2322
|
+
feature: "flows"
|
|
2323
|
+
});
|
|
1696
2324
|
}
|
|
1697
2325
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1698
2326
|
if (!dbFlow) {
|
|
1699
|
-
throw new
|
|
2327
|
+
throw new NotFoundError("Flow", flowId);
|
|
1700
2328
|
}
|
|
1701
2329
|
if (dbFlow.system) {
|
|
1702
|
-
throw new
|
|
2330
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1703
2331
|
}
|
|
1704
2332
|
const updated = await this.adapter.flows.update(flowId, {
|
|
1705
2333
|
status: "archived"
|
|
@@ -1711,14 +2339,16 @@ var FlowService = class {
|
|
|
1711
2339
|
*/
|
|
1712
2340
|
async deleteFlow(flowId) {
|
|
1713
2341
|
if (!this.adapter.flows) {
|
|
1714
|
-
throw new
|
|
2342
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2343
|
+
feature: "flows"
|
|
2344
|
+
});
|
|
1715
2345
|
}
|
|
1716
2346
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1717
2347
|
if (!dbFlow) {
|
|
1718
|
-
throw new
|
|
2348
|
+
throw new NotFoundError("Flow", flowId);
|
|
1719
2349
|
}
|
|
1720
2350
|
if (dbFlow.system) {
|
|
1721
|
-
throw new
|
|
2351
|
+
throw new ProtectedResourceError("object", dbFlow.name, "delete");
|
|
1722
2352
|
}
|
|
1723
2353
|
await this.adapter.flows.delete(flowId);
|
|
1724
2354
|
}
|
|
@@ -1730,16 +2360,23 @@ var FlowService = class {
|
|
|
1730
2360
|
*/
|
|
1731
2361
|
validateFlowName(name) {
|
|
1732
2362
|
if (!name || name.length === 0) {
|
|
1733
|
-
throw new
|
|
2363
|
+
throw new ValidationError("Flow name cannot be empty", [
|
|
2364
|
+
{ path: ["name"], message: "Flow name cannot be empty" }
|
|
2365
|
+
]);
|
|
1734
2366
|
}
|
|
1735
2367
|
if (name.length > 63) {
|
|
1736
|
-
throw new
|
|
2368
|
+
throw new ValidationError("Flow name is too long", [
|
|
2369
|
+
{ path: ["name"], message: "Flow name is too long (max 63 characters)" }
|
|
2370
|
+
]);
|
|
1737
2371
|
}
|
|
1738
2372
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
1739
2373
|
if (!kebabCaseRegex.test(name)) {
|
|
1740
|
-
throw new
|
|
1741
|
-
|
|
1742
|
-
|
|
2374
|
+
throw new ValidationError("Invalid flow name format", [
|
|
2375
|
+
{
|
|
2376
|
+
path: ["name"],
|
|
2377
|
+
message: "Flow name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
|
|
2378
|
+
}
|
|
2379
|
+
]);
|
|
1743
2380
|
}
|
|
1744
2381
|
}
|
|
1745
2382
|
/**
|
|
@@ -1747,33 +2384,59 @@ var FlowService = class {
|
|
|
1747
2384
|
*/
|
|
1748
2385
|
validateFlowStructure(input) {
|
|
1749
2386
|
if (!input.slots || input.slots.length === 0) {
|
|
1750
|
-
throw new
|
|
2387
|
+
throw new ValidationError("Flow must have at least one slot", [
|
|
2388
|
+
{ path: ["slots"], message: "Flow must have at least one slot" }
|
|
2389
|
+
]);
|
|
1751
2390
|
}
|
|
1752
2391
|
if (!input.pages || input.pages.length === 0) {
|
|
1753
|
-
throw new
|
|
2392
|
+
throw new ValidationError("Flow must have at least one page", [
|
|
2393
|
+
{ path: ["pages"], message: "Flow must have at least one page" }
|
|
2394
|
+
]);
|
|
1754
2395
|
}
|
|
1755
2396
|
const slotIds = new Set(input.slots.map((s) => s.id));
|
|
1756
2397
|
if (slotIds.size !== input.slots.length) {
|
|
1757
|
-
throw new
|
|
2398
|
+
throw new ValidationError("Duplicate slot IDs detected", [
|
|
2399
|
+
{ path: ["slots"], message: "Duplicate slot IDs detected" }
|
|
2400
|
+
]);
|
|
1758
2401
|
}
|
|
1759
2402
|
for (const page of input.pages) {
|
|
1760
2403
|
for (const row of page.rows) {
|
|
1761
2404
|
for (const field of row.fields) {
|
|
1762
2405
|
if (!slotIds.has(field.slotId)) {
|
|
1763
|
-
throw new
|
|
2406
|
+
throw new ValidationError("Field references unknown slot", [
|
|
2407
|
+
{
|
|
2408
|
+
path: ["pages", page.id, "rows", row.id, "fields", field.id],
|
|
2409
|
+
message: `Field "${field.id}" references unknown slot "${field.slotId}"`
|
|
2410
|
+
}
|
|
2411
|
+
]);
|
|
1764
2412
|
}
|
|
1765
2413
|
}
|
|
1766
2414
|
}
|
|
1767
2415
|
}
|
|
1768
2416
|
for (const relation of input.relations) {
|
|
1769
2417
|
if (!slotIds.has(relation.sourceSlotId)) {
|
|
1770
|
-
throw new
|
|
2418
|
+
throw new ValidationError("Relation references unknown source slot", [
|
|
2419
|
+
{
|
|
2420
|
+
path: ["relations", relation.id, "sourceSlotId"],
|
|
2421
|
+
message: `Relation references unknown source slot "${relation.sourceSlotId}"`
|
|
2422
|
+
}
|
|
2423
|
+
]);
|
|
1771
2424
|
}
|
|
1772
2425
|
if (!slotIds.has(relation.targetSlotId)) {
|
|
1773
|
-
throw new
|
|
2426
|
+
throw new ValidationError("Relation references unknown target slot", [
|
|
2427
|
+
{
|
|
2428
|
+
path: ["relations", relation.id, "targetSlotId"],
|
|
2429
|
+
message: `Relation references unknown target slot "${relation.targetSlotId}"`
|
|
2430
|
+
}
|
|
2431
|
+
]);
|
|
1774
2432
|
}
|
|
1775
2433
|
if (relation.sourceSlotId === relation.targetSlotId) {
|
|
1776
|
-
throw new
|
|
2434
|
+
throw new ValidationError("Relation cannot link a slot to itself", [
|
|
2435
|
+
{
|
|
2436
|
+
path: ["relations", relation.id],
|
|
2437
|
+
message: `Relation cannot link a slot to itself: "${relation.sourceSlotId}"`
|
|
2438
|
+
}
|
|
2439
|
+
]);
|
|
1777
2440
|
}
|
|
1778
2441
|
}
|
|
1779
2442
|
}
|
|
@@ -1917,8 +2580,8 @@ var GlobalSearchService = class {
|
|
|
1917
2580
|
};
|
|
1918
2581
|
|
|
1919
2582
|
// src/builders/attribute-validators.ts
|
|
1920
|
-
var
|
|
1921
|
-
var VALID_ICONS = new Set(
|
|
2583
|
+
var import_constants2 = require("@stndrds/constants");
|
|
2584
|
+
var VALID_ICONS = new Set(import_constants2.ICONS);
|
|
1922
2585
|
|
|
1923
2586
|
// src/builders/flow-builder.ts
|
|
1924
2587
|
var import_zod = require("zod");
|
|
@@ -2132,6 +2795,13 @@ var optionSchema = import_zod4.z.object({
|
|
|
2132
2795
|
description: import_zod4.z.string().optional(),
|
|
2133
2796
|
group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
|
|
2134
2797
|
});
|
|
2798
|
+
var optionsArraySchema = import_zod4.z.array(optionSchema).min(1).refine(
|
|
2799
|
+
(options) => {
|
|
2800
|
+
const values = options.map((o) => o.value);
|
|
2801
|
+
return new Set(values).size === values.length;
|
|
2802
|
+
},
|
|
2803
|
+
{ message: "Duplicate option values are not allowed" }
|
|
2804
|
+
);
|
|
2135
2805
|
var relationTargetSchema = import_zod4.z.object({
|
|
2136
2806
|
object: import_zod4.z.string().min(1),
|
|
2137
2807
|
displayTemplate: import_zod4.z.string().optional(),
|
|
@@ -2192,7 +2862,7 @@ var currencyConfigSchema = baseConfigSchema.extend({
|
|
|
2192
2862
|
allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
|
|
2193
2863
|
});
|
|
2194
2864
|
var statusConfigSchema = baseConfigSchema.extend({
|
|
2195
|
-
options:
|
|
2865
|
+
options: optionsArraySchema
|
|
2196
2866
|
});
|
|
2197
2867
|
var locationConfigSchema = baseConfigSchema.extend({
|
|
2198
2868
|
granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
|
|
@@ -2206,19 +2876,21 @@ var timestampConfigSchema = baseConfigSchema.extend({
|
|
|
2206
2876
|
autoUpdate: import_zod4.z.boolean().optional()
|
|
2207
2877
|
});
|
|
2208
2878
|
var selectConfigSchema = baseConfigSchema.extend({
|
|
2209
|
-
options:
|
|
2879
|
+
options: optionsArraySchema
|
|
2210
2880
|
});
|
|
2211
2881
|
var multiselectConfigSchema = baseConfigSchema.extend({
|
|
2212
|
-
options:
|
|
2882
|
+
options: optionsArraySchema
|
|
2213
2883
|
});
|
|
2214
2884
|
var fileConfigSchema = baseConfigSchema.extend({
|
|
2215
2885
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2216
2886
|
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2217
2887
|
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2218
|
-
verification: fileVerificationConfigSchema.optional()
|
|
2888
|
+
verification: fileVerificationConfigSchema.optional(),
|
|
2889
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2219
2890
|
});
|
|
2220
2891
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2221
|
-
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
|
|
2892
|
+
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2893
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2222
2894
|
});
|
|
2223
2895
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2224
2896
|
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
@@ -2340,20 +3012,39 @@ function createLocationValidator(_attr) {
|
|
|
2340
3012
|
function createTimestampValidator(_attr) {
|
|
2341
3013
|
return import_zod4.z.number().int().positive();
|
|
2342
3014
|
}
|
|
2343
|
-
function createFileValidator(
|
|
2344
|
-
|
|
3015
|
+
function createFileValidator(attr) {
|
|
3016
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3017
|
+
message: `${attr.label} must be a valid file ID`
|
|
3018
|
+
});
|
|
3019
|
+
if (attr.multiple) {
|
|
3020
|
+
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
3021
|
+
if (attr.maxFiles) {
|
|
3022
|
+
arraySchema = arraySchema.max(
|
|
3023
|
+
attr.maxFiles,
|
|
3024
|
+
`${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
return arraySchema;
|
|
3028
|
+
}
|
|
3029
|
+
return uuidSchema;
|
|
2345
3030
|
}
|
|
2346
|
-
function createUserValidator(
|
|
2347
|
-
|
|
3031
|
+
function createUserValidator(attr) {
|
|
3032
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3033
|
+
message: `${attr.label} must be a valid user ID`
|
|
3034
|
+
});
|
|
3035
|
+
if (attr.multiple) {
|
|
3036
|
+
return import_zod4.z.array(uuidSchema);
|
|
3037
|
+
}
|
|
3038
|
+
return uuidSchema;
|
|
2348
3039
|
}
|
|
2349
3040
|
function createSingleRelationValidator(attr) {
|
|
2350
|
-
const uuidSchema = import_zod4.z.
|
|
3041
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2351
3042
|
message: `${attr.label} must be a valid record ID`
|
|
2352
3043
|
});
|
|
2353
3044
|
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2354
3045
|
}
|
|
2355
3046
|
function createMultiRelationValidator(attr) {
|
|
2356
|
-
const uuidSchema = import_zod4.z.
|
|
3047
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2357
3048
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2358
3049
|
});
|
|
2359
3050
|
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
@@ -2871,14 +3562,15 @@ var ObjectSchemaService = class {
|
|
|
2871
3562
|
});
|
|
2872
3563
|
}
|
|
2873
3564
|
}
|
|
3565
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
3566
|
+
const attributes = dbAttributes.map((attr) => this.convertDBAttributeToAttribute(attr));
|
|
2874
3567
|
if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
|
|
2875
3568
|
const newExpression = updates.labelExpression;
|
|
2876
|
-
await this.adapter.objectRecords.batchRefreshLabels(
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
);
|
|
3569
|
+
await this.adapter.objectRecords.batchRefreshLabels(objectId, (values) => {
|
|
3570
|
+
const enrichedValues = enrichValuesWithSelectLabels(values, attributes);
|
|
3571
|
+
return renderLabelExpression(newExpression, enrichedValues);
|
|
3572
|
+
});
|
|
2880
3573
|
}
|
|
2881
|
-
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
2882
3574
|
return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
|
|
2883
3575
|
}
|
|
2884
3576
|
/**
|
|
@@ -3191,71 +3883,14 @@ var ObjectSchemaService = class {
|
|
|
3191
3883
|
}
|
|
3192
3884
|
};
|
|
3193
3885
|
|
|
3194
|
-
// src/exceptions.ts
|
|
3195
|
-
var SchemaErrorCode = {
|
|
3196
|
-
// Generic
|
|
3197
|
-
UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
|
|
3198
|
-
// Not Found
|
|
3199
|
-
OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
|
|
3200
|
-
ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
|
|
3201
|
-
RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
|
|
3202
|
-
USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
|
|
3203
|
-
FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
|
|
3204
|
-
ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
|
|
3205
|
-
// Validation
|
|
3206
|
-
VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
3207
|
-
INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
|
|
3208
|
-
INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
|
|
3209
|
-
// Protected Resources
|
|
3210
|
-
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
3211
|
-
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
3212
|
-
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
3213
|
-
// Permissions
|
|
3214
|
-
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
3215
|
-
// Sync
|
|
3216
|
-
SYNC_FAILED: "SCHEMA_SYNC_FAILED",
|
|
3217
|
-
NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
|
|
3218
|
-
// Duplicates
|
|
3219
|
-
DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
|
|
3220
|
-
DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
|
|
3221
|
-
};
|
|
3222
|
-
var SchemaError = class extends Error {
|
|
3223
|
-
constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
|
|
3224
|
-
super(message);
|
|
3225
|
-
this.name = "SchemaError";
|
|
3226
|
-
this.code = code;
|
|
3227
|
-
this.details = details;
|
|
3228
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
3229
|
-
}
|
|
3230
|
-
toJSON() {
|
|
3231
|
-
return {
|
|
3232
|
-
name: this.name,
|
|
3233
|
-
code: this.code,
|
|
3234
|
-
message: this.message,
|
|
3235
|
-
details: this.details
|
|
3236
|
-
};
|
|
3237
|
-
}
|
|
3238
|
-
};
|
|
3239
|
-
var ForbiddenError = class extends SchemaError {
|
|
3240
|
-
constructor(objectName, action, userId) {
|
|
3241
|
-
super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
|
|
3242
|
-
objectName,
|
|
3243
|
-
action,
|
|
3244
|
-
userId
|
|
3245
|
-
});
|
|
3246
|
-
this.name = "ForbiddenError";
|
|
3247
|
-
this.objectName = objectName;
|
|
3248
|
-
this.action = action;
|
|
3249
|
-
this.userId = userId;
|
|
3250
|
-
}
|
|
3251
|
-
};
|
|
3252
|
-
|
|
3253
3886
|
// src/runtime/services/permission.service.ts
|
|
3254
3887
|
var PermissionService = class {
|
|
3255
3888
|
constructor(adapter, tenantId, options) {
|
|
3256
3889
|
this.adapter = adapter;
|
|
3257
3890
|
this.tenantId = tenantId;
|
|
3258
3891
|
this.cache = /* @__PURE__ */ new Map();
|
|
3892
|
+
/** Track pending permission fetches to prevent duplicate concurrent requests */
|
|
3893
|
+
this.pendingFetches = /* @__PURE__ */ new Map();
|
|
3259
3894
|
if (!adapter.permissions) {
|
|
3260
3895
|
throw new Error(
|
|
3261
3896
|
"PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
|
|
@@ -3404,6 +4039,23 @@ var PermissionService = class {
|
|
|
3404
4039
|
if (cached && cached.expiresAt > Date.now()) {
|
|
3405
4040
|
return cached.permissions;
|
|
3406
4041
|
}
|
|
4042
|
+
const pendingFetch = this.pendingFetches.get(cacheKey);
|
|
4043
|
+
if (pendingFetch) {
|
|
4044
|
+
return pendingFetch;
|
|
4045
|
+
}
|
|
4046
|
+
const fetchPromise = this.fetchAndCachePermissions(userProfileId, cacheKey);
|
|
4047
|
+
this.pendingFetches.set(cacheKey, fetchPromise);
|
|
4048
|
+
try {
|
|
4049
|
+
return await fetchPromise;
|
|
4050
|
+
} finally {
|
|
4051
|
+
this.pendingFetches.delete(cacheKey);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Fetch permissions from database and cache the result
|
|
4056
|
+
* @internal
|
|
4057
|
+
*/
|
|
4058
|
+
async fetchAndCachePermissions(userProfileId, cacheKey) {
|
|
3407
4059
|
const permissions = await this.permissionsRepo.getEffectivePermissions(
|
|
3408
4060
|
userProfileId,
|
|
3409
4061
|
this.tenantId
|
|
@@ -3932,8 +4584,11 @@ var RelationService = class {
|
|
|
3932
4584
|
async validateRelationsOrThrow(schema, data) {
|
|
3933
4585
|
const result = await this.validateRelations(schema, data);
|
|
3934
4586
|
if (!result.valid) {
|
|
3935
|
-
const
|
|
3936
|
-
|
|
4587
|
+
const errors = result.errors.map((e) => ({
|
|
4588
|
+
path: [e.attribute],
|
|
4589
|
+
message: e.message
|
|
4590
|
+
}));
|
|
4591
|
+
throw new ValidationError("Relation validation failed", errors);
|
|
3937
4592
|
}
|
|
3938
4593
|
}
|
|
3939
4594
|
// ============================================================================
|
|
@@ -3981,7 +4636,8 @@ var RelationService = class {
|
|
|
3981
4636
|
totalCount += result.total;
|
|
3982
4637
|
for (const record of result.records) {
|
|
3983
4638
|
const template = target.displayTemplate || objectSchema.labelExpression;
|
|
3984
|
-
const
|
|
4639
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
4640
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
3985
4641
|
allOptions.push({
|
|
3986
4642
|
id: record.id,
|
|
3987
4643
|
objectId: objectSchema.id,
|
|
@@ -4045,7 +4701,8 @@ var RelationService = class {
|
|
|
4045
4701
|
}
|
|
4046
4702
|
}
|
|
4047
4703
|
for (const record of objectRecords) {
|
|
4048
|
-
const
|
|
4704
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
4705
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
4049
4706
|
resolved.push({
|
|
4050
4707
|
id: record.id,
|
|
4051
4708
|
objectId: record.objectId,
|
|
@@ -4066,10 +4723,131 @@ var RelationService = class {
|
|
|
4066
4723
|
if (!attribute || attribute.type !== "relation") {
|
|
4067
4724
|
return null;
|
|
4068
4725
|
}
|
|
4069
|
-
|
|
4726
|
+
const merged = {
|
|
4070
4727
|
...attribute.config,
|
|
4071
4728
|
...attribute
|
|
4072
4729
|
};
|
|
4730
|
+
if (!("targets" in merged && Array.isArray(merged.targets) && "cardinality" in merged)) {
|
|
4731
|
+
return null;
|
|
4732
|
+
}
|
|
4733
|
+
return merged;
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
|
|
4737
|
+
// src/runtime/services/user.service.ts
|
|
4738
|
+
var UserService = class {
|
|
4739
|
+
constructor(adapter, tenantId) {
|
|
4740
|
+
this.adapter = adapter;
|
|
4741
|
+
this.tenantId = tenantId;
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Validate all user attributes in the data
|
|
4745
|
+
*
|
|
4746
|
+
* @param schema - Object schema containing attribute definitions
|
|
4747
|
+
* @param data - Record data to validate
|
|
4748
|
+
* @returns Validation result with errors if any
|
|
4749
|
+
*
|
|
4750
|
+
* @example
|
|
4751
|
+
* ```typescript
|
|
4752
|
+
* const result = await userService.validateUsers(schema, {
|
|
4753
|
+
* assignee: "user-123",
|
|
4754
|
+
* watchers: ["user-456", "user-789"]
|
|
4755
|
+
* });
|
|
4756
|
+
*
|
|
4757
|
+
* if (!result.valid) {
|
|
4758
|
+
* console.log(result.errors);
|
|
4759
|
+
* // [{ attribute: "assignee", message: "User not found", invalidIds: ["user-123"] }]
|
|
4760
|
+
* }
|
|
4761
|
+
* ```
|
|
4762
|
+
*/
|
|
4763
|
+
async validateUsers(schema, data) {
|
|
4764
|
+
const errors = [];
|
|
4765
|
+
const userAttrs = schema.attributes.filter(
|
|
4766
|
+
(attr) => attr.type === "user"
|
|
4767
|
+
);
|
|
4768
|
+
for (const attr of userAttrs) {
|
|
4769
|
+
const value = data[attr.name];
|
|
4770
|
+
if (value === void 0 || value === null) {
|
|
4771
|
+
continue;
|
|
4772
|
+
}
|
|
4773
|
+
const attrErrors = await this.validateUserAttribute(attr, value);
|
|
4774
|
+
errors.push(...attrErrors);
|
|
4775
|
+
}
|
|
4776
|
+
return {
|
|
4777
|
+
valid: errors.length === 0,
|
|
4778
|
+
errors
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Validate a single user attribute value
|
|
4783
|
+
*/
|
|
4784
|
+
async validateUserAttribute(attr, value) {
|
|
4785
|
+
const errors = [];
|
|
4786
|
+
const ids = this.extractIds(attr, value);
|
|
4787
|
+
if (ids.length === 0) {
|
|
4788
|
+
return errors;
|
|
4789
|
+
}
|
|
4790
|
+
const invalidIds = [];
|
|
4791
|
+
const roleErrors = [];
|
|
4792
|
+
for (const id of ids) {
|
|
4793
|
+
const user = await this.adapter.userProfiles.findById(id);
|
|
4794
|
+
if (!user) {
|
|
4795
|
+
invalidIds.push(id);
|
|
4796
|
+
continue;
|
|
4797
|
+
}
|
|
4798
|
+
if (user.tenantId !== this.tenantId) {
|
|
4799
|
+
invalidIds.push(id);
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4802
|
+
if (attr.allowedRoles && attr.allowedRoles.length > 0) {
|
|
4803
|
+
if (!attr.allowedRoles.includes(user.role)) {
|
|
4804
|
+
roleErrors.push(id);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
if (invalidIds.length > 0) {
|
|
4809
|
+
errors.push({
|
|
4810
|
+
attribute: attr.name,
|
|
4811
|
+
message: `Invalid or non-existent users for ${attr.label}`,
|
|
4812
|
+
invalidIds
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
if (roleErrors.length > 0) {
|
|
4816
|
+
errors.push({
|
|
4817
|
+
attribute: attr.name,
|
|
4818
|
+
message: `Users do not have required role for ${attr.label}. Allowed roles: ${attr.allowedRoles?.join(", ")}`,
|
|
4819
|
+
invalidIds: roleErrors
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
return errors;
|
|
4823
|
+
}
|
|
4824
|
+
/**
|
|
4825
|
+
* Extract IDs from user value based on multiple flag
|
|
4826
|
+
*/
|
|
4827
|
+
extractIds(attr, value) {
|
|
4828
|
+
if (attr.multiple) {
|
|
4829
|
+
if (!Array.isArray(value)) {
|
|
4830
|
+
return [];
|
|
4831
|
+
}
|
|
4832
|
+
return value.filter((v) => typeof v === "string" && v !== "");
|
|
4833
|
+
}
|
|
4834
|
+
if (typeof value === "string" && value !== "") {
|
|
4835
|
+
return [value];
|
|
4836
|
+
}
|
|
4837
|
+
return [];
|
|
4838
|
+
}
|
|
4839
|
+
/**
|
|
4840
|
+
* Validate users and throw if invalid
|
|
4841
|
+
*/
|
|
4842
|
+
async validateUsersOrThrow(schema, data) {
|
|
4843
|
+
const result = await this.validateUsers(schema, data);
|
|
4844
|
+
if (!result.valid) {
|
|
4845
|
+
const errors = result.errors.map((e) => ({
|
|
4846
|
+
path: [e.attribute],
|
|
4847
|
+
message: e.message
|
|
4848
|
+
}));
|
|
4849
|
+
throw new ValidationError("User validation failed", errors);
|
|
4850
|
+
}
|
|
4073
4851
|
}
|
|
4074
4852
|
};
|
|
4075
4853
|
|
|
@@ -4080,9 +4858,10 @@ var RecordService = class {
|
|
|
4080
4858
|
this.tenantId = tenantId;
|
|
4081
4859
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
4082
4860
|
this.relationService = new RelationService(adapter, registry);
|
|
4861
|
+
this.userService = new UserService(adapter, tenantId);
|
|
4083
4862
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
4084
4863
|
this.permissionService = options?.permissionService;
|
|
4085
|
-
this.auditService = options?.auditService;
|
|
4864
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
4086
4865
|
this.userId = options?.userId;
|
|
4087
4866
|
this.userEmail = options?.userEmail;
|
|
4088
4867
|
}
|
|
@@ -4149,21 +4928,23 @@ var RecordService = class {
|
|
|
4149
4928
|
/**
|
|
4150
4929
|
* Compute display label from schema expression
|
|
4151
4930
|
* Automatically resolves relation attribute values to their labels
|
|
4931
|
+
* and select/multiselect values to their option labels
|
|
4152
4932
|
* @internal
|
|
4153
4933
|
*/
|
|
4154
4934
|
async computeLabel(schema, values) {
|
|
4155
4935
|
const attrNames = extractAttributeNames(schema.labelExpression);
|
|
4936
|
+
let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
|
|
4156
4937
|
const relationAttrs = schema.attributes.filter(
|
|
4157
4938
|
(attr) => attr.type === "relation" && attrNames.includes(attr.name)
|
|
4158
4939
|
);
|
|
4159
4940
|
if (relationAttrs.length === 0) {
|
|
4160
|
-
return renderLabelExpression(schema.labelExpression,
|
|
4941
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
4161
4942
|
}
|
|
4162
4943
|
const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
|
|
4163
4944
|
if (resolvedMap.size === 0) {
|
|
4164
|
-
return renderLabelExpression(schema.labelExpression,
|
|
4945
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
4165
4946
|
}
|
|
4166
|
-
|
|
4947
|
+
enrichedValues = { ...enrichedValues };
|
|
4167
4948
|
for (const attr of relationAttrs) {
|
|
4168
4949
|
const val = values[attr.name];
|
|
4169
4950
|
const ids = this.extractRelationIds(val);
|
|
@@ -4218,6 +4999,9 @@ var RecordService = class {
|
|
|
4218
4999
|
if (!options?.skipRelationValidation) {
|
|
4219
5000
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
4220
5001
|
}
|
|
5002
|
+
if (!options?.skipUserValidation) {
|
|
5003
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
5004
|
+
}
|
|
4221
5005
|
}
|
|
4222
5006
|
const completionStatus = computeRecordStatus(schema, data);
|
|
4223
5007
|
const label = await this.computeLabel(schema, data);
|
|
@@ -4276,7 +5060,7 @@ var RecordService = class {
|
|
|
4276
5060
|
async getRecordOrThrow(recordId) {
|
|
4277
5061
|
const record = await this.getRecord(recordId);
|
|
4278
5062
|
if (!record) {
|
|
4279
|
-
throw new
|
|
5063
|
+
throw new RecordNotFoundError(recordId);
|
|
4280
5064
|
}
|
|
4281
5065
|
return record;
|
|
4282
5066
|
}
|
|
@@ -4329,6 +5113,9 @@ var RecordService = class {
|
|
|
4329
5113
|
if (!options?.skipRelationValidation) {
|
|
4330
5114
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
4331
5115
|
}
|
|
5116
|
+
if (!options?.skipUserValidation) {
|
|
5117
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
5118
|
+
}
|
|
4332
5119
|
}
|
|
4333
5120
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
4334
5121
|
const label = await this.computeLabel(schema, mergedData);
|
|
@@ -4454,7 +5241,7 @@ var RecordService = class {
|
|
|
4454
5241
|
await this.checkPermission(schema.name, "delete");
|
|
4455
5242
|
if (options?.checkSystem) {
|
|
4456
5243
|
if (schema.system) {
|
|
4457
|
-
throw new
|
|
5244
|
+
throw new ProtectedResourceError("object", schema.name, "delete");
|
|
4458
5245
|
}
|
|
4459
5246
|
}
|
|
4460
5247
|
if (!options?.skipReferenceCheck) {
|
|
@@ -4503,7 +5290,11 @@ var RecordService = class {
|
|
|
4503
5290
|
async restoreRecord(recordId, options) {
|
|
4504
5291
|
const record = await this.getRecordOrThrow(recordId);
|
|
4505
5292
|
if (!record.deletedAt) {
|
|
4506
|
-
throw new
|
|
5293
|
+
throw new SchemaError(
|
|
5294
|
+
`Record "${recordId}" is not deleted`,
|
|
5295
|
+
SchemaErrorCode.VALIDATION_FAILED,
|
|
5296
|
+
{ recordId, reason: "not_deleted" }
|
|
5297
|
+
);
|
|
4507
5298
|
}
|
|
4508
5299
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
4509
5300
|
await this.checkPermission(schema.name, "update");
|
|
@@ -4941,11 +5732,17 @@ var ViewService = class {
|
|
|
4941
5732
|
this.validateViewName(input.name);
|
|
4942
5733
|
const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
|
|
4943
5734
|
if (existing) {
|
|
4944
|
-
throw new
|
|
5735
|
+
throw new SchemaError(
|
|
5736
|
+
`View "${input.name}" already exists for object "${input.objectName}"`,
|
|
5737
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
5738
|
+
{ viewName: input.name, objectName: input.objectName }
|
|
5739
|
+
);
|
|
4945
5740
|
}
|
|
4946
5741
|
if (this.nativeViews.has(input.objectName, input.name)) {
|
|
4947
|
-
throw new
|
|
4948
|
-
`Cannot create view "${input.name}": a system view with this name already exists
|
|
5742
|
+
throw new SchemaError(
|
|
5743
|
+
`Cannot create view "${input.name}": a system view with this name already exists`,
|
|
5744
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
5745
|
+
{ viewName: input.name, objectName: input.objectName, system: true }
|
|
4949
5746
|
);
|
|
4950
5747
|
}
|
|
4951
5748
|
const dbView = await this.adapter.views.create({
|
|
@@ -4973,10 +5770,10 @@ var ViewService = class {
|
|
|
4973
5770
|
async updateView(viewId, input) {
|
|
4974
5771
|
const dbView = await this.adapter.views.findById(viewId);
|
|
4975
5772
|
if (!dbView) {
|
|
4976
|
-
throw new
|
|
5773
|
+
throw new NotFoundError("View", viewId);
|
|
4977
5774
|
}
|
|
4978
5775
|
if (dbView.system) {
|
|
4979
|
-
throw new
|
|
5776
|
+
throw new ProtectedResourceError("view", dbView.name, "modify");
|
|
4980
5777
|
}
|
|
4981
5778
|
const updated = await this.adapter.views.update(viewId, {
|
|
4982
5779
|
label: input.label,
|
|
@@ -4996,10 +5793,10 @@ var ViewService = class {
|
|
|
4996
5793
|
async deleteView(viewId) {
|
|
4997
5794
|
const dbView = await this.adapter.views.findById(viewId);
|
|
4998
5795
|
if (!dbView) {
|
|
4999
|
-
throw new
|
|
5796
|
+
throw new NotFoundError("View", viewId);
|
|
5000
5797
|
}
|
|
5001
5798
|
if (dbView.system) {
|
|
5002
|
-
throw new
|
|
5799
|
+
throw new ProtectedResourceError("view", dbView.name, "delete");
|
|
5003
5800
|
}
|
|
5004
5801
|
await this.adapter.views.delete(viewId);
|
|
5005
5802
|
}
|
|
@@ -5013,7 +5810,7 @@ var ViewService = class {
|
|
|
5013
5810
|
async setDefaultView(viewId, tenantId) {
|
|
5014
5811
|
const dbView = await this.adapter.views.findById(viewId);
|
|
5015
5812
|
if (!dbView) {
|
|
5016
|
-
throw new
|
|
5813
|
+
throw new NotFoundError("View", viewId);
|
|
5017
5814
|
}
|
|
5018
5815
|
const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
|
|
5019
5816
|
for (const v of currentViews) {
|
|
@@ -5032,16 +5829,23 @@ var ViewService = class {
|
|
|
5032
5829
|
*/
|
|
5033
5830
|
validateViewName(name) {
|
|
5034
5831
|
if (!name || name.length === 0) {
|
|
5035
|
-
throw new
|
|
5832
|
+
throw new ValidationError("View name cannot be empty", [
|
|
5833
|
+
{ path: ["name"], message: "View name cannot be empty" }
|
|
5834
|
+
]);
|
|
5036
5835
|
}
|
|
5037
5836
|
if (name.length > 63) {
|
|
5038
|
-
throw new
|
|
5837
|
+
throw new ValidationError("View name is too long", [
|
|
5838
|
+
{ path: ["name"], message: "View name is too long (max 63 characters)" }
|
|
5839
|
+
]);
|
|
5039
5840
|
}
|
|
5040
5841
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
5041
5842
|
if (!kebabCaseRegex.test(name)) {
|
|
5042
|
-
throw new
|
|
5043
|
-
|
|
5044
|
-
|
|
5843
|
+
throw new ValidationError("Invalid view name format", [
|
|
5844
|
+
{
|
|
5845
|
+
path: ["name"],
|
|
5846
|
+
message: "View name must be in kebab-case (e.g., 'detail', 'list-view')"
|
|
5847
|
+
}
|
|
5848
|
+
]);
|
|
5045
5849
|
}
|
|
5046
5850
|
}
|
|
5047
5851
|
/**
|
|
@@ -5418,9 +6222,11 @@ var NoopGeocodingAdapter = class {
|
|
|
5418
6222
|
RecordService,
|
|
5419
6223
|
RelationService,
|
|
5420
6224
|
UserProfileService,
|
|
6225
|
+
UserService,
|
|
5421
6226
|
ViewService,
|
|
5422
6227
|
buildAuditChanges,
|
|
5423
6228
|
createMockAdapter,
|
|
6229
|
+
enrichValuesWithSelectLabels,
|
|
5424
6230
|
extractAttributeNames,
|
|
5425
6231
|
getSyncPreview,
|
|
5426
6232
|
getViewSyncPreview,
|