@stndrds/schema 0.1.0-alpha.16 → 0.1.0-alpha.17
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-4D23X5XN.mjs +7257 -0
- package/dist/chunk-6RPWHMTH.mjs +7101 -0
- package/dist/chunk-CMO3JEJL.mjs +7259 -0
- package/dist/chunk-JWRGX3UW.mjs +7265 -0
- package/dist/chunk-OUSLILLE.mjs +7259 -0
- package/dist/chunk-PMJZPJDP.mjs +7265 -0
- package/dist/chunk-YEV46K3M.mjs +7259 -0
- package/dist/index.d.mts +42 -3
- package/dist/index.d.ts +42 -3
- package/dist/index.js +398 -65
- package/dist/index.mjs +11 -1
- package/dist/runtime--rp-TG-h.d.mts +5181 -0
- package/dist/runtime--rp-TG-h.d.ts +5181 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +497 -124
- package/dist/runtime.mjs +3 -1
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -134,6 +134,7 @@ __export(runtime_exports, {
|
|
|
134
134
|
ViewService: () => ViewService,
|
|
135
135
|
buildAuditChanges: () => buildAuditChanges,
|
|
136
136
|
createMockAdapter: () => createMockAdapter,
|
|
137
|
+
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
137
138
|
extractAttributeNames: () => extractAttributeNames,
|
|
138
139
|
getSyncPreview: () => getSyncPreview,
|
|
139
140
|
getViewSyncPreview: () => getViewSyncPreview,
|
|
@@ -165,6 +166,168 @@ function generateId() {
|
|
|
165
166
|
});
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
// src/format.ts
|
|
170
|
+
var import_constants = require("@stndrds/constants");
|
|
171
|
+
var EMPTY_VALUE_PLACEHOLDER = "\u2014";
|
|
172
|
+
function formatText(value) {
|
|
173
|
+
return String(value);
|
|
174
|
+
}
|
|
175
|
+
function formatCheckbox(value) {
|
|
176
|
+
return value ? "Yes" : "No";
|
|
177
|
+
}
|
|
178
|
+
function formatNumber(value, attribute) {
|
|
179
|
+
if (typeof value !== "number") return String(value);
|
|
180
|
+
const decimals = attribute.decimals;
|
|
181
|
+
return value.toLocaleString(void 0, {
|
|
182
|
+
minimumFractionDigits: decimals,
|
|
183
|
+
maximumFractionDigits: decimals
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function formatCurrency(value, _attribute) {
|
|
187
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
188
|
+
const currency = value;
|
|
189
|
+
if (!("value" in currency && "code" in currency)) return String(value);
|
|
190
|
+
const formattedValue = currency.value.toLocaleString(void 0, {
|
|
191
|
+
minimumFractionDigits: 2,
|
|
192
|
+
maximumFractionDigits: 2
|
|
193
|
+
});
|
|
194
|
+
return `${formattedValue} ${currency.code}`;
|
|
195
|
+
}
|
|
196
|
+
function formatDate(value) {
|
|
197
|
+
if (value instanceof Date) {
|
|
198
|
+
return value.toISOString().split("T")[0];
|
|
199
|
+
}
|
|
200
|
+
if (typeof value === "string") {
|
|
201
|
+
const date = new Date(value);
|
|
202
|
+
if (!Number.isNaN(date.getTime())) {
|
|
203
|
+
return date.toISOString().split("T")[0];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return String(value);
|
|
207
|
+
}
|
|
208
|
+
function formatTimestamp(value) {
|
|
209
|
+
if (typeof value === "number") {
|
|
210
|
+
return new Date(value).toISOString();
|
|
211
|
+
}
|
|
212
|
+
if (value instanceof Date) {
|
|
213
|
+
return value.toISOString();
|
|
214
|
+
}
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
function formatPhone(value) {
|
|
218
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
219
|
+
const phone = value;
|
|
220
|
+
if (!("phoneNumber" in phone)) return String(value);
|
|
221
|
+
if (phone.countryCode) {
|
|
222
|
+
const country = (0, import_constants.getCountryByIso3)(phone.countryCode);
|
|
223
|
+
const dial = country?.phoneCode ?? "";
|
|
224
|
+
return `${dial} ${phone.phoneNumber}`.trim();
|
|
225
|
+
}
|
|
226
|
+
return phone.phoneNumber;
|
|
227
|
+
}
|
|
228
|
+
function formatLocation(value, attribute) {
|
|
229
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
230
|
+
const loc = value;
|
|
231
|
+
const granularity = attribute.granularity ?? "full";
|
|
232
|
+
const parts = [];
|
|
233
|
+
switch (granularity) {
|
|
234
|
+
case "country":
|
|
235
|
+
if (loc.country) parts.push(loc.country);
|
|
236
|
+
break;
|
|
237
|
+
case "state":
|
|
238
|
+
if (loc.state) parts.push(loc.state);
|
|
239
|
+
if (loc.country) parts.push(loc.country);
|
|
240
|
+
break;
|
|
241
|
+
case "city":
|
|
242
|
+
if (loc.city) parts.push(loc.city);
|
|
243
|
+
if (loc.state) parts.push(loc.state);
|
|
244
|
+
if (loc.country) parts.push(loc.country);
|
|
245
|
+
break;
|
|
246
|
+
case "coordinates":
|
|
247
|
+
if (loc.latitude !== void 0 && loc.longitude !== void 0) {
|
|
248
|
+
parts.push(`${loc.latitude}, ${loc.longitude}`);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case "address":
|
|
252
|
+
if (loc.address) parts.push(loc.address);
|
|
253
|
+
if (loc.city) parts.push(loc.city);
|
|
254
|
+
if (loc.state) parts.push(loc.state);
|
|
255
|
+
if (loc.country) parts.push(loc.country);
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
if (loc.address) parts.push(loc.address);
|
|
259
|
+
if (loc.city) parts.push(loc.city);
|
|
260
|
+
if (loc.state) parts.push(loc.state);
|
|
261
|
+
if (loc.postalCode) parts.push(loc.postalCode);
|
|
262
|
+
if (loc.country) parts.push(loc.country);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
return parts.join(", ") || EMPTY_VALUE_PLACEHOLDER;
|
|
266
|
+
}
|
|
267
|
+
function formatSelect(value, attribute) {
|
|
268
|
+
if (typeof value !== "string") return String(value);
|
|
269
|
+
const option = attribute.options?.find((o) => o.value === value);
|
|
270
|
+
return option?.label ?? String(value);
|
|
271
|
+
}
|
|
272
|
+
function formatMultiselect(value, attribute) {
|
|
273
|
+
if (!Array.isArray(value)) return String(value);
|
|
274
|
+
if (attribute.options) {
|
|
275
|
+
const labels = value.map((v) => attribute.options.find((o) => o.value === v)?.label).filter(Boolean);
|
|
276
|
+
return labels.join(", ");
|
|
277
|
+
}
|
|
278
|
+
return value.join(", ");
|
|
279
|
+
}
|
|
280
|
+
function formatRating(value, attribute) {
|
|
281
|
+
if (typeof value !== "number") return String(value);
|
|
282
|
+
const max = attribute.max ?? 5;
|
|
283
|
+
return `${value}/${max}`;
|
|
284
|
+
}
|
|
285
|
+
function formatAttributeValue(value, attribute) {
|
|
286
|
+
if (value === null || value === void 0 || value === "") {
|
|
287
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
288
|
+
}
|
|
289
|
+
switch (attribute.type) {
|
|
290
|
+
case "text":
|
|
291
|
+
case "textarea":
|
|
292
|
+
return formatText(value);
|
|
293
|
+
case "checkbox":
|
|
294
|
+
return formatCheckbox(value);
|
|
295
|
+
case "number":
|
|
296
|
+
return formatNumber(value, attribute);
|
|
297
|
+
case "currency":
|
|
298
|
+
return formatCurrency(value, attribute);
|
|
299
|
+
case "date":
|
|
300
|
+
return formatDate(value);
|
|
301
|
+
case "timestamp":
|
|
302
|
+
return formatTimestamp(value);
|
|
303
|
+
case "phone":
|
|
304
|
+
return formatPhone(value);
|
|
305
|
+
case "location":
|
|
306
|
+
return formatLocation(value, attribute);
|
|
307
|
+
case "select":
|
|
308
|
+
case "status":
|
|
309
|
+
return formatSelect(value, attribute);
|
|
310
|
+
case "multiselect":
|
|
311
|
+
return formatMultiselect(value, attribute);
|
|
312
|
+
case "rating":
|
|
313
|
+
return formatRating(value, attribute);
|
|
314
|
+
// Unsupported types - return value as-is or placeholder
|
|
315
|
+
case "file":
|
|
316
|
+
case "user":
|
|
317
|
+
case "relation":
|
|
318
|
+
if (Array.isArray(value)) {
|
|
319
|
+
return value.join(", ");
|
|
320
|
+
}
|
|
321
|
+
return String(value);
|
|
322
|
+
default: {
|
|
323
|
+
if (Array.isArray(value)) {
|
|
324
|
+
return value.join(", ");
|
|
325
|
+
}
|
|
326
|
+
return String(value);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
168
331
|
// src/runtime/template.ts
|
|
169
332
|
var pipes = {
|
|
170
333
|
/** Convert to uppercase */
|
|
@@ -216,6 +379,24 @@ function extractAttributeNames(template) {
|
|
|
216
379
|
}
|
|
217
380
|
return names;
|
|
218
381
|
}
|
|
382
|
+
function hasOptions(attr) {
|
|
383
|
+
return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
|
|
384
|
+
}
|
|
385
|
+
function enrichValuesWithSelectLabels(values, attributes) {
|
|
386
|
+
const enriched = { ...values };
|
|
387
|
+
for (const attr of attributes) {
|
|
388
|
+
const value = values[attr.name];
|
|
389
|
+
if (value == null) continue;
|
|
390
|
+
const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
|
|
391
|
+
if (!(isSelectLike && hasOptions(attr))) continue;
|
|
392
|
+
if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
|
|
393
|
+
const formatted = formatAttributeValue(value, attr);
|
|
394
|
+
if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
|
|
395
|
+
enriched[attr.name] = formatted;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return enriched;
|
|
399
|
+
}
|
|
219
400
|
|
|
220
401
|
// src/runtime/mock-adapter.ts
|
|
221
402
|
function createMockObjectsRepository(stores) {
|
|
@@ -692,14 +873,31 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
692
873
|
(options.offset ?? 0) + options.limit
|
|
693
874
|
);
|
|
694
875
|
}
|
|
876
|
+
const attributesByObjectId = /* @__PURE__ */ new Map();
|
|
877
|
+
for (const attr of stores.attributes.values()) {
|
|
878
|
+
if (!attributesByObjectId.has(attr.objectId)) {
|
|
879
|
+
attributesByObjectId.set(attr.objectId, []);
|
|
880
|
+
}
|
|
881
|
+
attributesByObjectId.get(attr.objectId)?.push(attr);
|
|
882
|
+
}
|
|
695
883
|
const results = matchingRecords.map((r) => {
|
|
696
884
|
const obj = objectsMap.get(r.objectId);
|
|
697
885
|
const labelExpression = obj?.labelExpression ?? "{{ name }}";
|
|
886
|
+
const dbAttrs = attributesByObjectId.get(r.objectId) ?? [];
|
|
887
|
+
const attrs = dbAttrs.map((a) => ({
|
|
888
|
+
...a.config,
|
|
889
|
+
id: a.id,
|
|
890
|
+
name: a.name,
|
|
891
|
+
type: a.type,
|
|
892
|
+
label: a.config.label ?? a.name,
|
|
893
|
+
required: a.config.required ?? false
|
|
894
|
+
}));
|
|
895
|
+
const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
|
|
698
896
|
return {
|
|
699
897
|
objectId: r.objectId,
|
|
700
898
|
objectName: obj?.name ?? "unknown",
|
|
701
899
|
objectLabel: obj?.label ?? "Unknown",
|
|
702
|
-
label: renderLabelExpression(labelExpression,
|
|
900
|
+
label: renderLabelExpression(labelExpression, enrichedValues),
|
|
703
901
|
recordId: r.id,
|
|
704
902
|
values: r.values,
|
|
705
903
|
completionStatus: r.completionStatus,
|
|
@@ -1116,6 +1314,10 @@ var AuditService = class {
|
|
|
1116
1314
|
this.options = options;
|
|
1117
1315
|
this.buffer = [];
|
|
1118
1316
|
this.flushTimer = null;
|
|
1317
|
+
/** Prevents concurrent flush operations */
|
|
1318
|
+
this.isFlushing = false;
|
|
1319
|
+
/** Pending flush promise to allow waiting on concurrent flush */
|
|
1320
|
+
this.flushPromise = null;
|
|
1119
1321
|
if (options?.async && options.flushIntervalMs) {
|
|
1120
1322
|
this.startFlushTimer();
|
|
1121
1323
|
}
|
|
@@ -1268,14 +1470,23 @@ var AuditService = class {
|
|
|
1268
1470
|
// ============================================================================
|
|
1269
1471
|
/**
|
|
1270
1472
|
* Flush buffered logs to the database
|
|
1473
|
+
* Protected against concurrent flush calls
|
|
1271
1474
|
*/
|
|
1272
1475
|
async flush() {
|
|
1476
|
+
if (this.isFlushing && this.flushPromise) {
|
|
1477
|
+
return this.flushPromise;
|
|
1478
|
+
}
|
|
1273
1479
|
if (this.buffer.length === 0 || !this.adapter.audit) {
|
|
1274
1480
|
return;
|
|
1275
1481
|
}
|
|
1482
|
+
this.isFlushing = true;
|
|
1276
1483
|
const entries = [...this.buffer];
|
|
1277
1484
|
this.buffer = [];
|
|
1278
|
-
|
|
1485
|
+
this.flushPromise = this.adapter.audit.createMany(entries).finally(() => {
|
|
1486
|
+
this.isFlushing = false;
|
|
1487
|
+
this.flushPromise = null;
|
|
1488
|
+
});
|
|
1489
|
+
return this.flushPromise;
|
|
1279
1490
|
}
|
|
1280
1491
|
/**
|
|
1281
1492
|
* Clean up resources (stop timer, flush remaining logs)
|
|
@@ -1297,6 +1508,12 @@ var AuditService = class {
|
|
|
1297
1508
|
if (!this.adapter.audit) {
|
|
1298
1509
|
return;
|
|
1299
1510
|
}
|
|
1511
|
+
if (entry.actorId && !entry.actorEmail && entry.actorType === "user") {
|
|
1512
|
+
const profile = await this.adapter.userProfiles.findById(entry.actorId);
|
|
1513
|
+
if (profile) {
|
|
1514
|
+
entry.actorEmail = profile.email;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1300
1517
|
if (this.options?.async) {
|
|
1301
1518
|
this.buffer.push(entry);
|
|
1302
1519
|
const batchSize = this.options.batchSize ?? 10;
|
|
@@ -1534,6 +1751,114 @@ var FileService = class {
|
|
|
1534
1751
|
}
|
|
1535
1752
|
};
|
|
1536
1753
|
|
|
1754
|
+
// src/exceptions.ts
|
|
1755
|
+
var SchemaErrorCode = {
|
|
1756
|
+
// Generic
|
|
1757
|
+
UNKNOWN: "SCHEMA_UNKNOWN_ERROR",
|
|
1758
|
+
// Not Found
|
|
1759
|
+
OBJECT_NOT_FOUND: "SCHEMA_OBJECT_NOT_FOUND",
|
|
1760
|
+
ATTRIBUTE_NOT_FOUND: "SCHEMA_ATTRIBUTE_NOT_FOUND",
|
|
1761
|
+
RECORD_NOT_FOUND: "SCHEMA_RECORD_NOT_FOUND",
|
|
1762
|
+
USER_PROFILE_NOT_FOUND: "SCHEMA_USER_PROFILE_NOT_FOUND",
|
|
1763
|
+
FILE_NOT_FOUND: "SCHEMA_FILE_NOT_FOUND",
|
|
1764
|
+
ROLE_NOT_FOUND: "SCHEMA_ROLE_NOT_FOUND",
|
|
1765
|
+
// Validation
|
|
1766
|
+
VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
1767
|
+
INVALID_ATTRIBUTE_NAME: "SCHEMA_INVALID_ATTRIBUTE_NAME",
|
|
1768
|
+
INVALID_OBJECT_NAME: "SCHEMA_INVALID_OBJECT_NAME",
|
|
1769
|
+
// Protected Resources
|
|
1770
|
+
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
1771
|
+
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
1772
|
+
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
1773
|
+
// Permissions
|
|
1774
|
+
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
1775
|
+
// Sync
|
|
1776
|
+
SYNC_FAILED: "SCHEMA_SYNC_FAILED",
|
|
1777
|
+
NOT_SYSTEM_OBJECT: "SCHEMA_NOT_SYSTEM_OBJECT",
|
|
1778
|
+
// Duplicates
|
|
1779
|
+
DUPLICATE_OBJECT: "SCHEMA_DUPLICATE_OBJECT",
|
|
1780
|
+
DUPLICATE_ATTRIBUTE: "SCHEMA_DUPLICATE_ATTRIBUTE"
|
|
1781
|
+
};
|
|
1782
|
+
var SchemaError = class extends Error {
|
|
1783
|
+
constructor(message, code = SchemaErrorCode.UNKNOWN, details) {
|
|
1784
|
+
super(message);
|
|
1785
|
+
this.name = "SchemaError";
|
|
1786
|
+
this.code = code;
|
|
1787
|
+
this.details = details;
|
|
1788
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1789
|
+
}
|
|
1790
|
+
toJSON() {
|
|
1791
|
+
return {
|
|
1792
|
+
name: this.name,
|
|
1793
|
+
code: this.code,
|
|
1794
|
+
message: this.message,
|
|
1795
|
+
details: this.details
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
var NotFoundError = class extends SchemaError {
|
|
1800
|
+
constructor(resourceType, resourceId, code = SchemaErrorCode.RECORD_NOT_FOUND) {
|
|
1801
|
+
super(`${resourceType} with id "${resourceId}" not found`, code, {
|
|
1802
|
+
resourceType,
|
|
1803
|
+
resourceId
|
|
1804
|
+
});
|
|
1805
|
+
this.name = "NotFoundError";
|
|
1806
|
+
this.resourceType = resourceType;
|
|
1807
|
+
this.resourceId = resourceId;
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
var RecordNotFoundError = class extends NotFoundError {
|
|
1811
|
+
constructor(recordId) {
|
|
1812
|
+
super("Record", recordId, SchemaErrorCode.RECORD_NOT_FOUND);
|
|
1813
|
+
this.name = "RecordNotFoundError";
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
var ValidationError = class _ValidationError extends SchemaError {
|
|
1817
|
+
constructor(message, errors) {
|
|
1818
|
+
super(message, SchemaErrorCode.VALIDATION_FAILED, { errors });
|
|
1819
|
+
this.name = "ValidationError";
|
|
1820
|
+
this.errors = errors;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Create a validation error from Zod-style errors
|
|
1824
|
+
*/
|
|
1825
|
+
static fromZodErrors(errors) {
|
|
1826
|
+
const details = errors.map((err) => ({
|
|
1827
|
+
path: err.path.map(String),
|
|
1828
|
+
message: err.message
|
|
1829
|
+
}));
|
|
1830
|
+
const message = `Validation failed: ${details.map((d) => `${d.path.join(".")}: ${d.message}`).join(", ")}`;
|
|
1831
|
+
return new _ValidationError(message, details);
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
var ProtectedResourceError = class extends SchemaError {
|
|
1835
|
+
constructor(resourceType, resourceName, operation) {
|
|
1836
|
+
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
1837
|
+
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
1838
|
+
resourceType,
|
|
1839
|
+
resourceName,
|
|
1840
|
+
operation
|
|
1841
|
+
});
|
|
1842
|
+
this.name = "ProtectedResourceError";
|
|
1843
|
+
this.resourceType = resourceType;
|
|
1844
|
+
this.resourceName = resourceName;
|
|
1845
|
+
this.operation = operation;
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
var ForbiddenError = class extends SchemaError {
|
|
1849
|
+
constructor(objectName, action, userId) {
|
|
1850
|
+
super(`No ${action} permission on object "${objectName}"`, SchemaErrorCode.FORBIDDEN, {
|
|
1851
|
+
objectName,
|
|
1852
|
+
action,
|
|
1853
|
+
userId
|
|
1854
|
+
});
|
|
1855
|
+
this.name = "ForbiddenError";
|
|
1856
|
+
this.objectName = objectName;
|
|
1857
|
+
this.action = action;
|
|
1858
|
+
this.userId = userId;
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1537
1862
|
// src/runtime/services/flow.service.ts
|
|
1538
1863
|
var FlowService = class {
|
|
1539
1864
|
constructor(adapter, systemFlows = []) {
|
|
@@ -1594,16 +1919,26 @@ var FlowService = class {
|
|
|
1594
1919
|
*/
|
|
1595
1920
|
async createFlow(input, tenantId) {
|
|
1596
1921
|
if (!this.adapter.flows) {
|
|
1597
|
-
throw new
|
|
1922
|
+
throw new SchemaError(
|
|
1923
|
+
"Flows feature is not enabled. Database adapter does not support flows.",
|
|
1924
|
+
SchemaErrorCode.UNKNOWN,
|
|
1925
|
+
{ feature: "flows" }
|
|
1926
|
+
);
|
|
1598
1927
|
}
|
|
1599
1928
|
this.validateFlowName(input.name);
|
|
1600
1929
|
const existing = await this.adapter.flows.findByName(tenantId, input.name);
|
|
1601
1930
|
if (existing) {
|
|
1602
|
-
throw new
|
|
1931
|
+
throw new SchemaError(
|
|
1932
|
+
`Flow "${input.name}" already exists`,
|
|
1933
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
1934
|
+
{ flowName: input.name }
|
|
1935
|
+
);
|
|
1603
1936
|
}
|
|
1604
1937
|
if (this.systemFlows.has(input.name)) {
|
|
1605
|
-
throw new
|
|
1606
|
-
`Cannot create flow "${input.name}": a system flow with this name already exists
|
|
1938
|
+
throw new SchemaError(
|
|
1939
|
+
`Cannot create flow "${input.name}": a system flow with this name already exists`,
|
|
1940
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
1941
|
+
{ flowName: input.name, system: true }
|
|
1607
1942
|
);
|
|
1608
1943
|
}
|
|
1609
1944
|
this.validateFlowStructure(input);
|
|
@@ -1628,14 +1963,16 @@ var FlowService = class {
|
|
|
1628
1963
|
*/
|
|
1629
1964
|
async updateFlow(flowId, input) {
|
|
1630
1965
|
if (!this.adapter.flows) {
|
|
1631
|
-
throw new
|
|
1966
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
1967
|
+
feature: "flows"
|
|
1968
|
+
});
|
|
1632
1969
|
}
|
|
1633
1970
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1634
1971
|
if (!dbFlow) {
|
|
1635
|
-
throw new
|
|
1972
|
+
throw new NotFoundError("Flow", flowId);
|
|
1636
1973
|
}
|
|
1637
1974
|
if (dbFlow.system) {
|
|
1638
|
-
throw new
|
|
1975
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1639
1976
|
}
|
|
1640
1977
|
if (input.slots || input.pages || input.relations) {
|
|
1641
1978
|
this.validateFlowStructure({
|
|
@@ -1662,14 +1999,16 @@ var FlowService = class {
|
|
|
1662
1999
|
*/
|
|
1663
2000
|
async publishFlow(flowId) {
|
|
1664
2001
|
if (!this.adapter.flows) {
|
|
1665
|
-
throw new
|
|
2002
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2003
|
+
feature: "flows"
|
|
2004
|
+
});
|
|
1666
2005
|
}
|
|
1667
2006
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1668
2007
|
if (!dbFlow) {
|
|
1669
|
-
throw new
|
|
2008
|
+
throw new NotFoundError("Flow", flowId);
|
|
1670
2009
|
}
|
|
1671
2010
|
if (dbFlow.system) {
|
|
1672
|
-
throw new
|
|
2011
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1673
2012
|
}
|
|
1674
2013
|
if (dbFlow.status === "published") {
|
|
1675
2014
|
return this.convertDBFlowToDefinition(dbFlow);
|
|
@@ -1692,14 +2031,16 @@ var FlowService = class {
|
|
|
1692
2031
|
*/
|
|
1693
2032
|
async archiveFlow(flowId) {
|
|
1694
2033
|
if (!this.adapter.flows) {
|
|
1695
|
-
throw new
|
|
2034
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2035
|
+
feature: "flows"
|
|
2036
|
+
});
|
|
1696
2037
|
}
|
|
1697
2038
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1698
2039
|
if (!dbFlow) {
|
|
1699
|
-
throw new
|
|
2040
|
+
throw new NotFoundError("Flow", flowId);
|
|
1700
2041
|
}
|
|
1701
2042
|
if (dbFlow.system) {
|
|
1702
|
-
throw new
|
|
2043
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
1703
2044
|
}
|
|
1704
2045
|
const updated = await this.adapter.flows.update(flowId, {
|
|
1705
2046
|
status: "archived"
|
|
@@ -1711,14 +2052,16 @@ var FlowService = class {
|
|
|
1711
2052
|
*/
|
|
1712
2053
|
async deleteFlow(flowId) {
|
|
1713
2054
|
if (!this.adapter.flows) {
|
|
1714
|
-
throw new
|
|
2055
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
2056
|
+
feature: "flows"
|
|
2057
|
+
});
|
|
1715
2058
|
}
|
|
1716
2059
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
1717
2060
|
if (!dbFlow) {
|
|
1718
|
-
throw new
|
|
2061
|
+
throw new NotFoundError("Flow", flowId);
|
|
1719
2062
|
}
|
|
1720
2063
|
if (dbFlow.system) {
|
|
1721
|
-
throw new
|
|
2064
|
+
throw new ProtectedResourceError("object", dbFlow.name, "delete");
|
|
1722
2065
|
}
|
|
1723
2066
|
await this.adapter.flows.delete(flowId);
|
|
1724
2067
|
}
|
|
@@ -1730,16 +2073,23 @@ var FlowService = class {
|
|
|
1730
2073
|
*/
|
|
1731
2074
|
validateFlowName(name) {
|
|
1732
2075
|
if (!name || name.length === 0) {
|
|
1733
|
-
throw new
|
|
2076
|
+
throw new ValidationError("Flow name cannot be empty", [
|
|
2077
|
+
{ path: ["name"], message: "Flow name cannot be empty" }
|
|
2078
|
+
]);
|
|
1734
2079
|
}
|
|
1735
2080
|
if (name.length > 63) {
|
|
1736
|
-
throw new
|
|
2081
|
+
throw new ValidationError("Flow name is too long", [
|
|
2082
|
+
{ path: ["name"], message: "Flow name is too long (max 63 characters)" }
|
|
2083
|
+
]);
|
|
1737
2084
|
}
|
|
1738
2085
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
1739
2086
|
if (!kebabCaseRegex.test(name)) {
|
|
1740
|
-
throw new
|
|
1741
|
-
|
|
1742
|
-
|
|
2087
|
+
throw new ValidationError("Invalid flow name format", [
|
|
2088
|
+
{
|
|
2089
|
+
path: ["name"],
|
|
2090
|
+
message: "Flow name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
|
|
2091
|
+
}
|
|
2092
|
+
]);
|
|
1743
2093
|
}
|
|
1744
2094
|
}
|
|
1745
2095
|
/**
|
|
@@ -1747,33 +2097,59 @@ var FlowService = class {
|
|
|
1747
2097
|
*/
|
|
1748
2098
|
validateFlowStructure(input) {
|
|
1749
2099
|
if (!input.slots || input.slots.length === 0) {
|
|
1750
|
-
throw new
|
|
2100
|
+
throw new ValidationError("Flow must have at least one slot", [
|
|
2101
|
+
{ path: ["slots"], message: "Flow must have at least one slot" }
|
|
2102
|
+
]);
|
|
1751
2103
|
}
|
|
1752
2104
|
if (!input.pages || input.pages.length === 0) {
|
|
1753
|
-
throw new
|
|
2105
|
+
throw new ValidationError("Flow must have at least one page", [
|
|
2106
|
+
{ path: ["pages"], message: "Flow must have at least one page" }
|
|
2107
|
+
]);
|
|
1754
2108
|
}
|
|
1755
2109
|
const slotIds = new Set(input.slots.map((s) => s.id));
|
|
1756
2110
|
if (slotIds.size !== input.slots.length) {
|
|
1757
|
-
throw new
|
|
2111
|
+
throw new ValidationError("Duplicate slot IDs detected", [
|
|
2112
|
+
{ path: ["slots"], message: "Duplicate slot IDs detected" }
|
|
2113
|
+
]);
|
|
1758
2114
|
}
|
|
1759
2115
|
for (const page of input.pages) {
|
|
1760
2116
|
for (const row of page.rows) {
|
|
1761
2117
|
for (const field of row.fields) {
|
|
1762
2118
|
if (!slotIds.has(field.slotId)) {
|
|
1763
|
-
throw new
|
|
2119
|
+
throw new ValidationError("Field references unknown slot", [
|
|
2120
|
+
{
|
|
2121
|
+
path: ["pages", page.id, "rows", row.id, "fields", field.id],
|
|
2122
|
+
message: `Field "${field.id}" references unknown slot "${field.slotId}"`
|
|
2123
|
+
}
|
|
2124
|
+
]);
|
|
1764
2125
|
}
|
|
1765
2126
|
}
|
|
1766
2127
|
}
|
|
1767
2128
|
}
|
|
1768
2129
|
for (const relation of input.relations) {
|
|
1769
2130
|
if (!slotIds.has(relation.sourceSlotId)) {
|
|
1770
|
-
throw new
|
|
2131
|
+
throw new ValidationError("Relation references unknown source slot", [
|
|
2132
|
+
{
|
|
2133
|
+
path: ["relations", relation.id, "sourceSlotId"],
|
|
2134
|
+
message: `Relation references unknown source slot "${relation.sourceSlotId}"`
|
|
2135
|
+
}
|
|
2136
|
+
]);
|
|
1771
2137
|
}
|
|
1772
2138
|
if (!slotIds.has(relation.targetSlotId)) {
|
|
1773
|
-
throw new
|
|
2139
|
+
throw new ValidationError("Relation references unknown target slot", [
|
|
2140
|
+
{
|
|
2141
|
+
path: ["relations", relation.id, "targetSlotId"],
|
|
2142
|
+
message: `Relation references unknown target slot "${relation.targetSlotId}"`
|
|
2143
|
+
}
|
|
2144
|
+
]);
|
|
1774
2145
|
}
|
|
1775
2146
|
if (relation.sourceSlotId === relation.targetSlotId) {
|
|
1776
|
-
throw new
|
|
2147
|
+
throw new ValidationError("Relation cannot link a slot to itself", [
|
|
2148
|
+
{
|
|
2149
|
+
path: ["relations", relation.id],
|
|
2150
|
+
message: `Relation cannot link a slot to itself: "${relation.sourceSlotId}"`
|
|
2151
|
+
}
|
|
2152
|
+
]);
|
|
1777
2153
|
}
|
|
1778
2154
|
}
|
|
1779
2155
|
}
|
|
@@ -1917,8 +2293,8 @@ var GlobalSearchService = class {
|
|
|
1917
2293
|
};
|
|
1918
2294
|
|
|
1919
2295
|
// src/builders/attribute-validators.ts
|
|
1920
|
-
var
|
|
1921
|
-
var VALID_ICONS = new Set(
|
|
2296
|
+
var import_constants2 = require("@stndrds/constants");
|
|
2297
|
+
var VALID_ICONS = new Set(import_constants2.ICONS);
|
|
1922
2298
|
|
|
1923
2299
|
// src/builders/flow-builder.ts
|
|
1924
2300
|
var import_zod = require("zod");
|
|
@@ -2132,6 +2508,13 @@ var optionSchema = import_zod4.z.object({
|
|
|
2132
2508
|
description: import_zod4.z.string().optional(),
|
|
2133
2509
|
group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
|
|
2134
2510
|
});
|
|
2511
|
+
var optionsArraySchema = import_zod4.z.array(optionSchema).min(1).refine(
|
|
2512
|
+
(options) => {
|
|
2513
|
+
const values = options.map((o) => o.value);
|
|
2514
|
+
return new Set(values).size === values.length;
|
|
2515
|
+
},
|
|
2516
|
+
{ message: "Duplicate option values are not allowed" }
|
|
2517
|
+
);
|
|
2135
2518
|
var relationTargetSchema = import_zod4.z.object({
|
|
2136
2519
|
object: import_zod4.z.string().min(1),
|
|
2137
2520
|
displayTemplate: import_zod4.z.string().optional(),
|
|
@@ -2192,7 +2575,7 @@ var currencyConfigSchema = baseConfigSchema.extend({
|
|
|
2192
2575
|
allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
|
|
2193
2576
|
});
|
|
2194
2577
|
var statusConfigSchema = baseConfigSchema.extend({
|
|
2195
|
-
options:
|
|
2578
|
+
options: optionsArraySchema
|
|
2196
2579
|
});
|
|
2197
2580
|
var locationConfigSchema = baseConfigSchema.extend({
|
|
2198
2581
|
granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
|
|
@@ -2206,10 +2589,10 @@ var timestampConfigSchema = baseConfigSchema.extend({
|
|
|
2206
2589
|
autoUpdate: import_zod4.z.boolean().optional()
|
|
2207
2590
|
});
|
|
2208
2591
|
var selectConfigSchema = baseConfigSchema.extend({
|
|
2209
|
-
options:
|
|
2592
|
+
options: optionsArraySchema
|
|
2210
2593
|
});
|
|
2211
2594
|
var multiselectConfigSchema = baseConfigSchema.extend({
|
|
2212
|
-
options:
|
|
2595
|
+
options: optionsArraySchema
|
|
2213
2596
|
});
|
|
2214
2597
|
var fileConfigSchema = baseConfigSchema.extend({
|
|
2215
2598
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
@@ -2871,14 +3254,15 @@ var ObjectSchemaService = class {
|
|
|
2871
3254
|
});
|
|
2872
3255
|
}
|
|
2873
3256
|
}
|
|
3257
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
3258
|
+
const attributes = dbAttributes.map((attr) => this.convertDBAttributeToAttribute(attr));
|
|
2874
3259
|
if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
|
|
2875
3260
|
const newExpression = updates.labelExpression;
|
|
2876
|
-
await this.adapter.objectRecords.batchRefreshLabels(
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
);
|
|
3261
|
+
await this.adapter.objectRecords.batchRefreshLabels(objectId, (values) => {
|
|
3262
|
+
const enrichedValues = enrichValuesWithSelectLabels(values, attributes);
|
|
3263
|
+
return renderLabelExpression(newExpression, enrichedValues);
|
|
3264
|
+
});
|
|
2880
3265
|
}
|
|
2881
|
-
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
2882
3266
|
return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
|
|
2883
3267
|
}
|
|
2884
3268
|
/**
|
|
@@ -3191,71 +3575,14 @@ var ObjectSchemaService = class {
|
|
|
3191
3575
|
}
|
|
3192
3576
|
};
|
|
3193
3577
|
|
|
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
3578
|
// src/runtime/services/permission.service.ts
|
|
3254
3579
|
var PermissionService = class {
|
|
3255
3580
|
constructor(adapter, tenantId, options) {
|
|
3256
3581
|
this.adapter = adapter;
|
|
3257
3582
|
this.tenantId = tenantId;
|
|
3258
3583
|
this.cache = /* @__PURE__ */ new Map();
|
|
3584
|
+
/** Track pending permission fetches to prevent duplicate concurrent requests */
|
|
3585
|
+
this.pendingFetches = /* @__PURE__ */ new Map();
|
|
3259
3586
|
if (!adapter.permissions) {
|
|
3260
3587
|
throw new Error(
|
|
3261
3588
|
"PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
|
|
@@ -3404,6 +3731,23 @@ var PermissionService = class {
|
|
|
3404
3731
|
if (cached && cached.expiresAt > Date.now()) {
|
|
3405
3732
|
return cached.permissions;
|
|
3406
3733
|
}
|
|
3734
|
+
const pendingFetch = this.pendingFetches.get(cacheKey);
|
|
3735
|
+
if (pendingFetch) {
|
|
3736
|
+
return pendingFetch;
|
|
3737
|
+
}
|
|
3738
|
+
const fetchPromise = this.fetchAndCachePermissions(userProfileId, cacheKey);
|
|
3739
|
+
this.pendingFetches.set(cacheKey, fetchPromise);
|
|
3740
|
+
try {
|
|
3741
|
+
return await fetchPromise;
|
|
3742
|
+
} finally {
|
|
3743
|
+
this.pendingFetches.delete(cacheKey);
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Fetch permissions from database and cache the result
|
|
3748
|
+
* @internal
|
|
3749
|
+
*/
|
|
3750
|
+
async fetchAndCachePermissions(userProfileId, cacheKey) {
|
|
3407
3751
|
const permissions = await this.permissionsRepo.getEffectivePermissions(
|
|
3408
3752
|
userProfileId,
|
|
3409
3753
|
this.tenantId
|
|
@@ -3932,8 +4276,11 @@ var RelationService = class {
|
|
|
3932
4276
|
async validateRelationsOrThrow(schema, data) {
|
|
3933
4277
|
const result = await this.validateRelations(schema, data);
|
|
3934
4278
|
if (!result.valid) {
|
|
3935
|
-
const
|
|
3936
|
-
|
|
4279
|
+
const errors = result.errors.map((e) => ({
|
|
4280
|
+
path: [e.attribute],
|
|
4281
|
+
message: e.message
|
|
4282
|
+
}));
|
|
4283
|
+
throw new ValidationError("Relation validation failed", errors);
|
|
3937
4284
|
}
|
|
3938
4285
|
}
|
|
3939
4286
|
// ============================================================================
|
|
@@ -3981,7 +4328,8 @@ var RelationService = class {
|
|
|
3981
4328
|
totalCount += result.total;
|
|
3982
4329
|
for (const record of result.records) {
|
|
3983
4330
|
const template = target.displayTemplate || objectSchema.labelExpression;
|
|
3984
|
-
const
|
|
4331
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
4332
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
3985
4333
|
allOptions.push({
|
|
3986
4334
|
id: record.id,
|
|
3987
4335
|
objectId: objectSchema.id,
|
|
@@ -4045,7 +4393,8 @@ var RelationService = class {
|
|
|
4045
4393
|
}
|
|
4046
4394
|
}
|
|
4047
4395
|
for (const record of objectRecords) {
|
|
4048
|
-
const
|
|
4396
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
4397
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
4049
4398
|
resolved.push({
|
|
4050
4399
|
id: record.id,
|
|
4051
4400
|
objectId: record.objectId,
|
|
@@ -4066,10 +4415,14 @@ var RelationService = class {
|
|
|
4066
4415
|
if (!attribute || attribute.type !== "relation") {
|
|
4067
4416
|
return null;
|
|
4068
4417
|
}
|
|
4069
|
-
|
|
4418
|
+
const merged = {
|
|
4070
4419
|
...attribute.config,
|
|
4071
4420
|
...attribute
|
|
4072
4421
|
};
|
|
4422
|
+
if (!("targets" in merged && Array.isArray(merged.targets) && "cardinality" in merged)) {
|
|
4423
|
+
return null;
|
|
4424
|
+
}
|
|
4425
|
+
return merged;
|
|
4073
4426
|
}
|
|
4074
4427
|
};
|
|
4075
4428
|
|
|
@@ -4082,7 +4435,7 @@ var RecordService = class {
|
|
|
4082
4435
|
this.relationService = new RelationService(adapter, registry);
|
|
4083
4436
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
4084
4437
|
this.permissionService = options?.permissionService;
|
|
4085
|
-
this.auditService = options?.auditService;
|
|
4438
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
4086
4439
|
this.userId = options?.userId;
|
|
4087
4440
|
this.userEmail = options?.userEmail;
|
|
4088
4441
|
}
|
|
@@ -4149,21 +4502,23 @@ var RecordService = class {
|
|
|
4149
4502
|
/**
|
|
4150
4503
|
* Compute display label from schema expression
|
|
4151
4504
|
* Automatically resolves relation attribute values to their labels
|
|
4505
|
+
* and select/multiselect values to their option labels
|
|
4152
4506
|
* @internal
|
|
4153
4507
|
*/
|
|
4154
4508
|
async computeLabel(schema, values) {
|
|
4155
4509
|
const attrNames = extractAttributeNames(schema.labelExpression);
|
|
4510
|
+
let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
|
|
4156
4511
|
const relationAttrs = schema.attributes.filter(
|
|
4157
4512
|
(attr) => attr.type === "relation" && attrNames.includes(attr.name)
|
|
4158
4513
|
);
|
|
4159
4514
|
if (relationAttrs.length === 0) {
|
|
4160
|
-
return renderLabelExpression(schema.labelExpression,
|
|
4515
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
4161
4516
|
}
|
|
4162
4517
|
const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
|
|
4163
4518
|
if (resolvedMap.size === 0) {
|
|
4164
|
-
return renderLabelExpression(schema.labelExpression,
|
|
4519
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
4165
4520
|
}
|
|
4166
|
-
|
|
4521
|
+
enrichedValues = { ...enrichedValues };
|
|
4167
4522
|
for (const attr of relationAttrs) {
|
|
4168
4523
|
const val = values[attr.name];
|
|
4169
4524
|
const ids = this.extractRelationIds(val);
|
|
@@ -4276,7 +4631,7 @@ var RecordService = class {
|
|
|
4276
4631
|
async getRecordOrThrow(recordId) {
|
|
4277
4632
|
const record = await this.getRecord(recordId);
|
|
4278
4633
|
if (!record) {
|
|
4279
|
-
throw new
|
|
4634
|
+
throw new RecordNotFoundError(recordId);
|
|
4280
4635
|
}
|
|
4281
4636
|
return record;
|
|
4282
4637
|
}
|
|
@@ -4454,7 +4809,7 @@ var RecordService = class {
|
|
|
4454
4809
|
await this.checkPermission(schema.name, "delete");
|
|
4455
4810
|
if (options?.checkSystem) {
|
|
4456
4811
|
if (schema.system) {
|
|
4457
|
-
throw new
|
|
4812
|
+
throw new ProtectedResourceError("object", schema.name, "delete");
|
|
4458
4813
|
}
|
|
4459
4814
|
}
|
|
4460
4815
|
if (!options?.skipReferenceCheck) {
|
|
@@ -4503,7 +4858,11 @@ var RecordService = class {
|
|
|
4503
4858
|
async restoreRecord(recordId, options) {
|
|
4504
4859
|
const record = await this.getRecordOrThrow(recordId);
|
|
4505
4860
|
if (!record.deletedAt) {
|
|
4506
|
-
throw new
|
|
4861
|
+
throw new SchemaError(
|
|
4862
|
+
`Record "${recordId}" is not deleted`,
|
|
4863
|
+
SchemaErrorCode.VALIDATION_FAILED,
|
|
4864
|
+
{ recordId, reason: "not_deleted" }
|
|
4865
|
+
);
|
|
4507
4866
|
}
|
|
4508
4867
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
4509
4868
|
await this.checkPermission(schema.name, "update");
|
|
@@ -4941,11 +5300,17 @@ var ViewService = class {
|
|
|
4941
5300
|
this.validateViewName(input.name);
|
|
4942
5301
|
const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
|
|
4943
5302
|
if (existing) {
|
|
4944
|
-
throw new
|
|
5303
|
+
throw new SchemaError(
|
|
5304
|
+
`View "${input.name}" already exists for object "${input.objectName}"`,
|
|
5305
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
5306
|
+
{ viewName: input.name, objectName: input.objectName }
|
|
5307
|
+
);
|
|
4945
5308
|
}
|
|
4946
5309
|
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
|
|
5310
|
+
throw new SchemaError(
|
|
5311
|
+
`Cannot create view "${input.name}": a system view with this name already exists`,
|
|
5312
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
5313
|
+
{ viewName: input.name, objectName: input.objectName, system: true }
|
|
4949
5314
|
);
|
|
4950
5315
|
}
|
|
4951
5316
|
const dbView = await this.adapter.views.create({
|
|
@@ -4973,10 +5338,10 @@ var ViewService = class {
|
|
|
4973
5338
|
async updateView(viewId, input) {
|
|
4974
5339
|
const dbView = await this.adapter.views.findById(viewId);
|
|
4975
5340
|
if (!dbView) {
|
|
4976
|
-
throw new
|
|
5341
|
+
throw new NotFoundError("View", viewId);
|
|
4977
5342
|
}
|
|
4978
5343
|
if (dbView.system) {
|
|
4979
|
-
throw new
|
|
5344
|
+
throw new ProtectedResourceError("attribute", dbView.name, "modify");
|
|
4980
5345
|
}
|
|
4981
5346
|
const updated = await this.adapter.views.update(viewId, {
|
|
4982
5347
|
label: input.label,
|
|
@@ -4996,10 +5361,10 @@ var ViewService = class {
|
|
|
4996
5361
|
async deleteView(viewId) {
|
|
4997
5362
|
const dbView = await this.adapter.views.findById(viewId);
|
|
4998
5363
|
if (!dbView) {
|
|
4999
|
-
throw new
|
|
5364
|
+
throw new NotFoundError("View", viewId);
|
|
5000
5365
|
}
|
|
5001
5366
|
if (dbView.system) {
|
|
5002
|
-
throw new
|
|
5367
|
+
throw new ProtectedResourceError("attribute", dbView.name, "delete");
|
|
5003
5368
|
}
|
|
5004
5369
|
await this.adapter.views.delete(viewId);
|
|
5005
5370
|
}
|
|
@@ -5013,7 +5378,7 @@ var ViewService = class {
|
|
|
5013
5378
|
async setDefaultView(viewId, tenantId) {
|
|
5014
5379
|
const dbView = await this.adapter.views.findById(viewId);
|
|
5015
5380
|
if (!dbView) {
|
|
5016
|
-
throw new
|
|
5381
|
+
throw new NotFoundError("View", viewId);
|
|
5017
5382
|
}
|
|
5018
5383
|
const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
|
|
5019
5384
|
for (const v of currentViews) {
|
|
@@ -5032,16 +5397,23 @@ var ViewService = class {
|
|
|
5032
5397
|
*/
|
|
5033
5398
|
validateViewName(name) {
|
|
5034
5399
|
if (!name || name.length === 0) {
|
|
5035
|
-
throw new
|
|
5400
|
+
throw new ValidationError("View name cannot be empty", [
|
|
5401
|
+
{ path: ["name"], message: "View name cannot be empty" }
|
|
5402
|
+
]);
|
|
5036
5403
|
}
|
|
5037
5404
|
if (name.length > 63) {
|
|
5038
|
-
throw new
|
|
5405
|
+
throw new ValidationError("View name is too long", [
|
|
5406
|
+
{ path: ["name"], message: "View name is too long (max 63 characters)" }
|
|
5407
|
+
]);
|
|
5039
5408
|
}
|
|
5040
5409
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
5041
5410
|
if (!kebabCaseRegex.test(name)) {
|
|
5042
|
-
throw new
|
|
5043
|
-
|
|
5044
|
-
|
|
5411
|
+
throw new ValidationError("Invalid view name format", [
|
|
5412
|
+
{
|
|
5413
|
+
path: ["name"],
|
|
5414
|
+
message: "View name must be in kebab-case (e.g., 'detail', 'list-view')"
|
|
5415
|
+
}
|
|
5416
|
+
]);
|
|
5045
5417
|
}
|
|
5046
5418
|
}
|
|
5047
5419
|
/**
|
|
@@ -5421,6 +5793,7 @@ var NoopGeocodingAdapter = class {
|
|
|
5421
5793
|
ViewService,
|
|
5422
5794
|
buildAuditChanges,
|
|
5423
5795
|
createMockAdapter,
|
|
5796
|
+
enrichValuesWithSelectLabels,
|
|
5424
5797
|
extractAttributeNames,
|
|
5425
5798
|
getSyncPreview,
|
|
5426
5799
|
getViewSyncPreview,
|