@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/index.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 */
|
|
@@ -129,6 +133,7 @@ __export(index_exports, {
|
|
|
129
133
|
DEFAULT_ROLE_LABELS: () => DEFAULT_ROLE_LABELS,
|
|
130
134
|
DEFAULT_ROLE_PERMISSIONS: () => DEFAULT_ROLE_PERMISSIONS,
|
|
131
135
|
DuplicateError: () => DuplicateError,
|
|
136
|
+
EMPTY_VALUE_PLACEHOLDER: () => EMPTY_VALUE_PLACEHOLDER,
|
|
132
137
|
FileNotFoundError: () => FileNotFoundError,
|
|
133
138
|
FileService: () => FileService,
|
|
134
139
|
FlowBuilder: () => FlowBuilder,
|
|
@@ -166,6 +171,7 @@ __export(index_exports, {
|
|
|
166
171
|
TableTabConfig: () => TableTabConfig,
|
|
167
172
|
UserProfileNotFoundError: () => UserProfileNotFoundError,
|
|
168
173
|
UserProfileService: () => UserProfileService,
|
|
174
|
+
UserService: () => UserService,
|
|
169
175
|
ValidationError: () => ValidationError,
|
|
170
176
|
ViewBuilder: () => ViewBuilder,
|
|
171
177
|
ViewService: () => ViewService,
|
|
@@ -199,10 +205,12 @@ __export(index_exports, {
|
|
|
199
205
|
currencyConfigSchema: () => currencyConfigSchema,
|
|
200
206
|
date: () => date,
|
|
201
207
|
dateConfigSchema: () => dateConfigSchema,
|
|
208
|
+
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
202
209
|
extractAttributeNames: () => extractAttributeNames,
|
|
203
210
|
file: () => file,
|
|
204
211
|
fileConfigSchema: () => fileConfigSchema,
|
|
205
212
|
flow: () => flow,
|
|
213
|
+
formatAttributeValue: () => formatAttributeValue,
|
|
206
214
|
generateId: () => generateId,
|
|
207
215
|
generatePrefixedId: () => generatePrefixedId,
|
|
208
216
|
getAttributeConfigSchema: () => getAttributeConfigSchema,
|
|
@@ -210,6 +218,7 @@ __export(index_exports, {
|
|
|
210
218
|
getSyncPreview: () => getSyncPreview,
|
|
211
219
|
getViewSyncPreview: () => getViewSyncPreview,
|
|
212
220
|
group: () => group,
|
|
221
|
+
isActivityTab: () => isActivityTab,
|
|
213
222
|
isAdvancedFilterState: () => isAdvancedFilterState,
|
|
214
223
|
isCustomTab: () => isCustomTab,
|
|
215
224
|
isDefaultRole: () => isDefaultRole,
|
|
@@ -434,6 +443,9 @@ function isTableTab(tab) {
|
|
|
434
443
|
function isCustomTab(tab) {
|
|
435
444
|
return tab.type === "custom";
|
|
436
445
|
}
|
|
446
|
+
function isActivityTab(tab) {
|
|
447
|
+
return tab.type === "activity";
|
|
448
|
+
}
|
|
437
449
|
|
|
438
450
|
// src/utils.ts
|
|
439
451
|
function generateId() {
|
|
@@ -450,6 +462,175 @@ function generatePrefixedId(prefix) {
|
|
|
450
462
|
return `${prefix}_${generateId()}`;
|
|
451
463
|
}
|
|
452
464
|
|
|
465
|
+
// src/format.ts
|
|
466
|
+
var import_constants = require("@stndrds/constants");
|
|
467
|
+
var EMPTY_VALUE_PLACEHOLDER = "\u2014";
|
|
468
|
+
function formatText(value) {
|
|
469
|
+
return String(value);
|
|
470
|
+
}
|
|
471
|
+
function formatCheckbox(value) {
|
|
472
|
+
return value ? "Yes" : "No";
|
|
473
|
+
}
|
|
474
|
+
function formatNumber(value, attribute) {
|
|
475
|
+
if (typeof value !== "number") return String(value);
|
|
476
|
+
const decimals = attribute.decimals;
|
|
477
|
+
if (attribute.unit === "percentage") {
|
|
478
|
+
return value.toLocaleString(void 0, {
|
|
479
|
+
style: "percent",
|
|
480
|
+
minimumFractionDigits: decimals,
|
|
481
|
+
maximumFractionDigits: decimals
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return value.toLocaleString(void 0, {
|
|
485
|
+
minimumFractionDigits: decimals,
|
|
486
|
+
maximumFractionDigits: decimals
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function formatCurrency(value, _attribute) {
|
|
490
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
491
|
+
const currency2 = value;
|
|
492
|
+
if (!("value" in currency2 && "code" in currency2)) return String(value);
|
|
493
|
+
const formattedValue = currency2.value.toLocaleString(void 0, {
|
|
494
|
+
minimumFractionDigits: 2,
|
|
495
|
+
maximumFractionDigits: 2
|
|
496
|
+
});
|
|
497
|
+
return `${formattedValue} ${currency2.code}`;
|
|
498
|
+
}
|
|
499
|
+
function formatDate(value) {
|
|
500
|
+
if (value instanceof Date) {
|
|
501
|
+
return value.toISOString().split("T")[0];
|
|
502
|
+
}
|
|
503
|
+
if (typeof value === "string") {
|
|
504
|
+
const date2 = new Date(value);
|
|
505
|
+
if (!Number.isNaN(date2.getTime())) {
|
|
506
|
+
return date2.toISOString().split("T")[0];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return String(value);
|
|
510
|
+
}
|
|
511
|
+
function formatTimestamp(value) {
|
|
512
|
+
if (typeof value === "number") {
|
|
513
|
+
return new Date(value).toISOString();
|
|
514
|
+
}
|
|
515
|
+
if (value instanceof Date) {
|
|
516
|
+
return value.toISOString();
|
|
517
|
+
}
|
|
518
|
+
return String(value);
|
|
519
|
+
}
|
|
520
|
+
function formatPhone(value) {
|
|
521
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
522
|
+
const phone2 = value;
|
|
523
|
+
if (!("phoneNumber" in phone2)) return String(value);
|
|
524
|
+
if (phone2.countryCode) {
|
|
525
|
+
const country = (0, import_constants.getCountryByIso3)(phone2.countryCode);
|
|
526
|
+
const dial = country?.phoneCode ?? "";
|
|
527
|
+
return `${dial} ${phone2.phoneNumber}`.trim();
|
|
528
|
+
}
|
|
529
|
+
return phone2.phoneNumber;
|
|
530
|
+
}
|
|
531
|
+
function formatLocation(value, attribute) {
|
|
532
|
+
if (typeof value !== "object" || value === null) return String(value);
|
|
533
|
+
const loc = value;
|
|
534
|
+
const granularity = attribute.granularity ?? "full";
|
|
535
|
+
const parts = [];
|
|
536
|
+
switch (granularity) {
|
|
537
|
+
case "country":
|
|
538
|
+
if (loc.country) parts.push(loc.country);
|
|
539
|
+
break;
|
|
540
|
+
case "state":
|
|
541
|
+
if (loc.state) parts.push(loc.state);
|
|
542
|
+
if (loc.country) parts.push(loc.country);
|
|
543
|
+
break;
|
|
544
|
+
case "city":
|
|
545
|
+
if (loc.city) parts.push(loc.city);
|
|
546
|
+
if (loc.state) parts.push(loc.state);
|
|
547
|
+
if (loc.country) parts.push(loc.country);
|
|
548
|
+
break;
|
|
549
|
+
case "coordinates":
|
|
550
|
+
if (loc.latitude !== void 0 && loc.longitude !== void 0) {
|
|
551
|
+
parts.push(`${loc.latitude}, ${loc.longitude}`);
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
case "address":
|
|
555
|
+
if (loc.address) parts.push(loc.address);
|
|
556
|
+
if (loc.city) parts.push(loc.city);
|
|
557
|
+
if (loc.state) parts.push(loc.state);
|
|
558
|
+
if (loc.country) parts.push(loc.country);
|
|
559
|
+
break;
|
|
560
|
+
default:
|
|
561
|
+
if (loc.address) parts.push(loc.address);
|
|
562
|
+
if (loc.city) parts.push(loc.city);
|
|
563
|
+
if (loc.state) parts.push(loc.state);
|
|
564
|
+
if (loc.postalCode) parts.push(loc.postalCode);
|
|
565
|
+
if (loc.country) parts.push(loc.country);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
return parts.join(", ") || EMPTY_VALUE_PLACEHOLDER;
|
|
569
|
+
}
|
|
570
|
+
function formatSelect(value, attribute) {
|
|
571
|
+
if (typeof value !== "string") return String(value);
|
|
572
|
+
const option = attribute.options?.find((o) => o.value === value);
|
|
573
|
+
return option?.label ?? String(value);
|
|
574
|
+
}
|
|
575
|
+
function formatMultiselect(value, attribute) {
|
|
576
|
+
if (!Array.isArray(value)) return String(value);
|
|
577
|
+
if (attribute.options) {
|
|
578
|
+
const labels = value.map((v) => attribute.options.find((o) => o.value === v)?.label).filter(Boolean);
|
|
579
|
+
return labels.join(", ");
|
|
580
|
+
}
|
|
581
|
+
return value.join(", ");
|
|
582
|
+
}
|
|
583
|
+
function formatRating(value, attribute) {
|
|
584
|
+
if (typeof value !== "number") return String(value);
|
|
585
|
+
const max = attribute.max ?? 5;
|
|
586
|
+
return `${value}/${max}`;
|
|
587
|
+
}
|
|
588
|
+
function formatAttributeValue(value, attribute) {
|
|
589
|
+
if (value === null || value === void 0 || value === "") {
|
|
590
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
591
|
+
}
|
|
592
|
+
switch (attribute.type) {
|
|
593
|
+
case "text":
|
|
594
|
+
case "textarea":
|
|
595
|
+
return formatText(value);
|
|
596
|
+
case "checkbox":
|
|
597
|
+
return formatCheckbox(value);
|
|
598
|
+
case "number":
|
|
599
|
+
return formatNumber(value, attribute);
|
|
600
|
+
case "currency":
|
|
601
|
+
return formatCurrency(value, attribute);
|
|
602
|
+
case "date":
|
|
603
|
+
return formatDate(value);
|
|
604
|
+
case "timestamp":
|
|
605
|
+
return formatTimestamp(value);
|
|
606
|
+
case "phone":
|
|
607
|
+
return formatPhone(value);
|
|
608
|
+
case "location":
|
|
609
|
+
return formatLocation(value, attribute);
|
|
610
|
+
case "select":
|
|
611
|
+
case "status":
|
|
612
|
+
return formatSelect(value, attribute);
|
|
613
|
+
case "multiselect":
|
|
614
|
+
return formatMultiselect(value, attribute);
|
|
615
|
+
case "rating":
|
|
616
|
+
return formatRating(value, attribute);
|
|
617
|
+
// Unsupported types - return value as-is or placeholder
|
|
618
|
+
case "file":
|
|
619
|
+
case "user":
|
|
620
|
+
case "relation":
|
|
621
|
+
if (Array.isArray(value)) {
|
|
622
|
+
return value.join(", ");
|
|
623
|
+
}
|
|
624
|
+
return String(value);
|
|
625
|
+
default: {
|
|
626
|
+
if (Array.isArray(value)) {
|
|
627
|
+
return value.join(", ");
|
|
628
|
+
}
|
|
629
|
+
return String(value);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
453
634
|
// src/constants/index.ts
|
|
454
635
|
init_default_roles();
|
|
455
636
|
|
|
@@ -471,6 +652,7 @@ var SchemaErrorCode = {
|
|
|
471
652
|
// Protected Resources
|
|
472
653
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
473
654
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
655
|
+
PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
|
|
474
656
|
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
475
657
|
// Permissions
|
|
476
658
|
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
@@ -559,7 +741,7 @@ var ValidationError = class _ValidationError extends SchemaError {
|
|
|
559
741
|
};
|
|
560
742
|
var ProtectedResourceError = class extends SchemaError {
|
|
561
743
|
constructor(resourceType, resourceName, operation) {
|
|
562
|
-
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
744
|
+
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
563
745
|
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
564
746
|
resourceType,
|
|
565
747
|
resourceName,
|
|
@@ -653,8 +835,8 @@ function isForbiddenError(error) {
|
|
|
653
835
|
}
|
|
654
836
|
|
|
655
837
|
// src/builders/attribute-validators.ts
|
|
656
|
-
var
|
|
657
|
-
var VALID_ICONS = new Set(
|
|
838
|
+
var import_constants2 = require("@stndrds/constants");
|
|
839
|
+
var VALID_ICONS = new Set(import_constants2.ICONS);
|
|
658
840
|
var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
659
841
|
var MAX_NAME_LENGTH = 63;
|
|
660
842
|
var MAX_LABEL_LENGTH = 128;
|
|
@@ -1155,6 +1337,13 @@ var FileAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1155
1337
|
this.attr.verification = config;
|
|
1156
1338
|
return this;
|
|
1157
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Allow multiple files to be uploaded
|
|
1342
|
+
*/
|
|
1343
|
+
multiple() {
|
|
1344
|
+
this.attr.multiple = true;
|
|
1345
|
+
return this;
|
|
1346
|
+
}
|
|
1158
1347
|
required() {
|
|
1159
1348
|
this.setRequired(true);
|
|
1160
1349
|
return this;
|
|
@@ -1175,6 +1364,13 @@ var UserAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1175
1364
|
this.attr.allowedRoles = roles;
|
|
1176
1365
|
return this;
|
|
1177
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Allow multiple users to be selected
|
|
1369
|
+
*/
|
|
1370
|
+
multiple() {
|
|
1371
|
+
this.attr.multiple = true;
|
|
1372
|
+
return this;
|
|
1373
|
+
}
|
|
1178
1374
|
required() {
|
|
1179
1375
|
this.setRequired(true);
|
|
1180
1376
|
return this;
|
|
@@ -2491,6 +2687,13 @@ var optionSchema = import_zod4.z.object({
|
|
|
2491
2687
|
description: import_zod4.z.string().optional(),
|
|
2492
2688
|
group: import_zod4.z.enum(["idle", "in_progress", "finished"]).optional()
|
|
2493
2689
|
});
|
|
2690
|
+
var optionsArraySchema = import_zod4.z.array(optionSchema).min(1).refine(
|
|
2691
|
+
(options) => {
|
|
2692
|
+
const values = options.map((o) => o.value);
|
|
2693
|
+
return new Set(values).size === values.length;
|
|
2694
|
+
},
|
|
2695
|
+
{ message: "Duplicate option values are not allowed" }
|
|
2696
|
+
);
|
|
2494
2697
|
var relationTargetSchema = import_zod4.z.object({
|
|
2495
2698
|
object: import_zod4.z.string().min(1),
|
|
2496
2699
|
displayTemplate: import_zod4.z.string().optional(),
|
|
@@ -2551,7 +2754,7 @@ var currencyConfigSchema = baseConfigSchema.extend({
|
|
|
2551
2754
|
allowedCurrencies: import_zod4.z.array(import_zod4.z.string().length(3)).optional()
|
|
2552
2755
|
});
|
|
2553
2756
|
var statusConfigSchema = baseConfigSchema.extend({
|
|
2554
|
-
options:
|
|
2757
|
+
options: optionsArraySchema
|
|
2555
2758
|
});
|
|
2556
2759
|
var locationConfigSchema = baseConfigSchema.extend({
|
|
2557
2760
|
granularity: import_zod4.z.enum(["full", "address", "city", "state", "country", "coordinates"]),
|
|
@@ -2565,19 +2768,21 @@ var timestampConfigSchema = baseConfigSchema.extend({
|
|
|
2565
2768
|
autoUpdate: import_zod4.z.boolean().optional()
|
|
2566
2769
|
});
|
|
2567
2770
|
var selectConfigSchema = baseConfigSchema.extend({
|
|
2568
|
-
options:
|
|
2771
|
+
options: optionsArraySchema
|
|
2569
2772
|
});
|
|
2570
2773
|
var multiselectConfigSchema = baseConfigSchema.extend({
|
|
2571
|
-
options:
|
|
2774
|
+
options: optionsArraySchema
|
|
2572
2775
|
});
|
|
2573
2776
|
var fileConfigSchema = baseConfigSchema.extend({
|
|
2574
2777
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2575
2778
|
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2576
2779
|
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2577
|
-
verification: fileVerificationConfigSchema.optional()
|
|
2780
|
+
verification: fileVerificationConfigSchema.optional(),
|
|
2781
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2578
2782
|
});
|
|
2579
2783
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2580
|
-
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
|
|
2784
|
+
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2785
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2581
2786
|
});
|
|
2582
2787
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2583
2788
|
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
@@ -2715,20 +2920,39 @@ function createLocationValidator(_attr) {
|
|
|
2715
2920
|
function createTimestampValidator(_attr) {
|
|
2716
2921
|
return import_zod4.z.number().int().positive();
|
|
2717
2922
|
}
|
|
2718
|
-
function createFileValidator(
|
|
2719
|
-
|
|
2923
|
+
function createFileValidator(attr) {
|
|
2924
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2925
|
+
message: `${attr.label} must be a valid file ID`
|
|
2926
|
+
});
|
|
2927
|
+
if (attr.multiple) {
|
|
2928
|
+
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
2929
|
+
if (attr.maxFiles) {
|
|
2930
|
+
arraySchema = arraySchema.max(
|
|
2931
|
+
attr.maxFiles,
|
|
2932
|
+
`${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
return arraySchema;
|
|
2936
|
+
}
|
|
2937
|
+
return uuidSchema;
|
|
2720
2938
|
}
|
|
2721
|
-
function createUserValidator(
|
|
2722
|
-
|
|
2939
|
+
function createUserValidator(attr) {
|
|
2940
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2941
|
+
message: `${attr.label} must be a valid user ID`
|
|
2942
|
+
});
|
|
2943
|
+
if (attr.multiple) {
|
|
2944
|
+
return import_zod4.z.array(uuidSchema);
|
|
2945
|
+
}
|
|
2946
|
+
return uuidSchema;
|
|
2723
2947
|
}
|
|
2724
2948
|
function createSingleRelationValidator(attr) {
|
|
2725
|
-
const uuidSchema = import_zod4.z.
|
|
2949
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2726
2950
|
message: `${attr.label} must be a valid record ID`
|
|
2727
2951
|
});
|
|
2728
2952
|
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2729
2953
|
}
|
|
2730
2954
|
function createMultiRelationValidator(attr) {
|
|
2731
|
-
const uuidSchema = import_zod4.z.
|
|
2955
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2732
2956
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2733
2957
|
});
|
|
2734
2958
|
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
@@ -2973,6 +3197,24 @@ function extractAttributeNames(template) {
|
|
|
2973
3197
|
}
|
|
2974
3198
|
return names;
|
|
2975
3199
|
}
|
|
3200
|
+
function hasOptions(attr) {
|
|
3201
|
+
return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
|
|
3202
|
+
}
|
|
3203
|
+
function enrichValuesWithSelectLabels(values, attributes) {
|
|
3204
|
+
const enriched = { ...values };
|
|
3205
|
+
for (const attr of attributes) {
|
|
3206
|
+
const value = values[attr.name];
|
|
3207
|
+
if (value == null) continue;
|
|
3208
|
+
const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
|
|
3209
|
+
if (!(isSelectLike && hasOptions(attr))) continue;
|
|
3210
|
+
if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
|
|
3211
|
+
const formatted = formatAttributeValue(value, attr);
|
|
3212
|
+
if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
|
|
3213
|
+
enriched[attr.name] = formatted;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
return enriched;
|
|
3217
|
+
}
|
|
2976
3218
|
|
|
2977
3219
|
// src/runtime/mock-adapter.ts
|
|
2978
3220
|
function createMockObjectsRepository(stores) {
|
|
@@ -3449,14 +3691,31 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
3449
3691
|
(options.offset ?? 0) + options.limit
|
|
3450
3692
|
);
|
|
3451
3693
|
}
|
|
3694
|
+
const attributesByObjectId = /* @__PURE__ */ new Map();
|
|
3695
|
+
for (const attr of stores.attributes.values()) {
|
|
3696
|
+
if (!attributesByObjectId.has(attr.objectId)) {
|
|
3697
|
+
attributesByObjectId.set(attr.objectId, []);
|
|
3698
|
+
}
|
|
3699
|
+
attributesByObjectId.get(attr.objectId)?.push(attr);
|
|
3700
|
+
}
|
|
3452
3701
|
const results = matchingRecords.map((r) => {
|
|
3453
3702
|
const obj = objectsMap.get(r.objectId);
|
|
3454
3703
|
const labelExpression = obj?.labelExpression ?? "{{ name }}";
|
|
3704
|
+
const dbAttrs = attributesByObjectId.get(r.objectId) ?? [];
|
|
3705
|
+
const attrs = dbAttrs.map((a) => ({
|
|
3706
|
+
...a.config,
|
|
3707
|
+
id: a.id,
|
|
3708
|
+
name: a.name,
|
|
3709
|
+
type: a.type,
|
|
3710
|
+
label: a.config.label ?? a.name,
|
|
3711
|
+
required: a.config.required ?? false
|
|
3712
|
+
}));
|
|
3713
|
+
const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
|
|
3455
3714
|
return {
|
|
3456
3715
|
objectId: r.objectId,
|
|
3457
3716
|
objectName: obj?.name ?? "unknown",
|
|
3458
3717
|
objectLabel: obj?.label ?? "Unknown",
|
|
3459
|
-
label: renderLabelExpression(labelExpression,
|
|
3718
|
+
label: renderLabelExpression(labelExpression, enrichedValues),
|
|
3460
3719
|
recordId: r.id,
|
|
3461
3720
|
values: r.values,
|
|
3462
3721
|
completionStatus: r.completionStatus,
|
|
@@ -3873,6 +4132,10 @@ var AuditService = class {
|
|
|
3873
4132
|
this.options = options;
|
|
3874
4133
|
this.buffer = [];
|
|
3875
4134
|
this.flushTimer = null;
|
|
4135
|
+
/** Prevents concurrent flush operations */
|
|
4136
|
+
this.isFlushing = false;
|
|
4137
|
+
/** Pending flush promise to allow waiting on concurrent flush */
|
|
4138
|
+
this.flushPromise = null;
|
|
3876
4139
|
if (options?.async && options.flushIntervalMs) {
|
|
3877
4140
|
this.startFlushTimer();
|
|
3878
4141
|
}
|
|
@@ -3959,6 +4222,24 @@ var AuditService = class {
|
|
|
3959
4222
|
};
|
|
3960
4223
|
await this.log(entry);
|
|
3961
4224
|
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Log a file action (upload, update, delete)
|
|
4227
|
+
*/
|
|
4228
|
+
async logFileAction(params) {
|
|
4229
|
+
const entry = {
|
|
4230
|
+
tenantId: this.tenantId,
|
|
4231
|
+
actorId: params.actorId,
|
|
4232
|
+
actorEmail: params.actorEmail,
|
|
4233
|
+
actorType: "user",
|
|
4234
|
+
action: params.action,
|
|
4235
|
+
resourceType: "file",
|
|
4236
|
+
resourceId: params.fileId,
|
|
4237
|
+
resourceLabel: params.fileName,
|
|
4238
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
4239
|
+
metadata: params.metadata
|
|
4240
|
+
};
|
|
4241
|
+
await this.log(entry);
|
|
4242
|
+
}
|
|
3962
4243
|
/**
|
|
3963
4244
|
* Log a system action (no actor)
|
|
3964
4245
|
*/
|
|
@@ -4025,14 +4306,23 @@ var AuditService = class {
|
|
|
4025
4306
|
// ============================================================================
|
|
4026
4307
|
/**
|
|
4027
4308
|
* Flush buffered logs to the database
|
|
4309
|
+
* Protected against concurrent flush calls
|
|
4028
4310
|
*/
|
|
4029
4311
|
async flush() {
|
|
4312
|
+
if (this.isFlushing && this.flushPromise) {
|
|
4313
|
+
return this.flushPromise;
|
|
4314
|
+
}
|
|
4030
4315
|
if (this.buffer.length === 0 || !this.adapter.audit) {
|
|
4031
4316
|
return;
|
|
4032
4317
|
}
|
|
4318
|
+
this.isFlushing = true;
|
|
4033
4319
|
const entries = [...this.buffer];
|
|
4034
4320
|
this.buffer = [];
|
|
4035
|
-
|
|
4321
|
+
this.flushPromise = this.adapter.audit.createMany(entries).finally(() => {
|
|
4322
|
+
this.isFlushing = false;
|
|
4323
|
+
this.flushPromise = null;
|
|
4324
|
+
});
|
|
4325
|
+
return this.flushPromise;
|
|
4036
4326
|
}
|
|
4037
4327
|
/**
|
|
4038
4328
|
* Clean up resources (stop timer, flush remaining logs)
|
|
@@ -4054,6 +4344,12 @@ var AuditService = class {
|
|
|
4054
4344
|
if (!this.adapter.audit) {
|
|
4055
4345
|
return;
|
|
4056
4346
|
}
|
|
4347
|
+
if (entry.actorId && !entry.actorEmail && entry.actorType === "user") {
|
|
4348
|
+
const profile = await this.adapter.userProfiles.findById(entry.actorId);
|
|
4349
|
+
if (profile) {
|
|
4350
|
+
entry.actorEmail = profile.email;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4057
4353
|
if (this.options?.async) {
|
|
4058
4354
|
this.buffer.push(entry);
|
|
4059
4355
|
const batchSize = this.options.batchSize ?? 10;
|
|
@@ -4093,21 +4389,105 @@ var AuditService = class {
|
|
|
4093
4389
|
|
|
4094
4390
|
// src/runtime/services/file.service.ts
|
|
4095
4391
|
var FileService = class {
|
|
4096
|
-
constructor(adapter, tenantId) {
|
|
4392
|
+
constructor(adapter, tenantId, options) {
|
|
4097
4393
|
this.adapter = adapter;
|
|
4098
4394
|
this.tenantId = tenantId;
|
|
4395
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
4396
|
+
this.userId = options?.userId;
|
|
4397
|
+
this.userEmail = options?.userEmail;
|
|
4099
4398
|
}
|
|
4399
|
+
// ============================================================================
|
|
4400
|
+
// UPLOAD (requires StorageAdapter)
|
|
4401
|
+
// ============================================================================
|
|
4100
4402
|
/**
|
|
4101
|
-
*
|
|
4403
|
+
* Upload a file to storage and create metadata record.
|
|
4102
4404
|
*
|
|
4103
|
-
*
|
|
4405
|
+
* This method orchestrates:
|
|
4406
|
+
* 1. Upload to storage (via StorageAdapter)
|
|
4407
|
+
* 2. Create file metadata in database
|
|
4408
|
+
* 3. Audit log the operation
|
|
4409
|
+
*
|
|
4410
|
+
* Requires `adapter.storage` to be configured.
|
|
4411
|
+
*
|
|
4412
|
+
* @param input - File content and metadata
|
|
4104
4413
|
* @returns Created file record
|
|
4414
|
+
* @throws Error if StorageAdapter is not configured
|
|
4105
4415
|
*
|
|
4106
4416
|
* @example
|
|
4107
4417
|
* ```typescript
|
|
4108
|
-
* const
|
|
4418
|
+
* const file = await service.uploadFile({
|
|
4419
|
+
* content: fileBuffer,
|
|
4420
|
+
* fileName: "document.pdf",
|
|
4421
|
+
* mimeType: "application/pdf",
|
|
4422
|
+
* size: 12345,
|
|
4423
|
+
* uploadedBy: "user-123",
|
|
4424
|
+
* visibility: "private",
|
|
4425
|
+
* folderPath: "/documents",
|
|
4426
|
+
* tags: ["contract", "2025"],
|
|
4427
|
+
* });
|
|
4428
|
+
* ```
|
|
4429
|
+
*/
|
|
4430
|
+
async uploadFile(input) {
|
|
4431
|
+
if (!this.adapter.storage) {
|
|
4432
|
+
throw new Error(
|
|
4433
|
+
"StorageAdapter is not configured. Provide adapter.storage to use uploadFile()."
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
const uploadResult = await this.adapter.storage.upload({
|
|
4437
|
+
content: input.content,
|
|
4438
|
+
fileName: input.fileName,
|
|
4439
|
+
mimeType: input.mimeType,
|
|
4440
|
+
size: input.size,
|
|
4441
|
+
tenantId: this.tenantId,
|
|
4442
|
+
folderPath: input.folderPath
|
|
4443
|
+
});
|
|
4444
|
+
const file2 = await this.adapter.files.create({
|
|
4445
|
+
tenantId: this.tenantId,
|
|
4446
|
+
name: input.fileName,
|
|
4447
|
+
originalName: input.fileName,
|
|
4448
|
+
mimeType: input.mimeType,
|
|
4449
|
+
size: input.size,
|
|
4450
|
+
storageProvider: uploadResult.storageProvider,
|
|
4451
|
+
storagePath: uploadResult.storagePath,
|
|
4452
|
+
storageBucket: uploadResult.storageBucket,
|
|
4453
|
+
url: uploadResult.url,
|
|
4454
|
+
uploadedBy: input.uploadedBy,
|
|
4455
|
+
folderPath: input.folderPath,
|
|
4456
|
+
tags: input.tags,
|
|
4457
|
+
visibility: input.visibility ?? "private",
|
|
4458
|
+
allowedUsers: input.allowedUsers
|
|
4459
|
+
});
|
|
4460
|
+
if (this.auditService && this.userId) {
|
|
4461
|
+
await this.auditService.logFileAction({
|
|
4462
|
+
action: "file.uploaded",
|
|
4463
|
+
actorId: this.userId,
|
|
4464
|
+
actorEmail: this.userEmail,
|
|
4465
|
+
fileId: file2.id,
|
|
4466
|
+
fileName: file2.name,
|
|
4467
|
+
metadata: {
|
|
4468
|
+
mimeType: file2.mimeType,
|
|
4469
|
+
size: file2.size,
|
|
4470
|
+
visibility: file2.visibility
|
|
4471
|
+
}
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
return file2;
|
|
4475
|
+
}
|
|
4476
|
+
// ============================================================================
|
|
4477
|
+
// CREATE (metadata only, for external storage)
|
|
4478
|
+
// ============================================================================
|
|
4479
|
+
/**
|
|
4480
|
+
* Create a new file record (after upload to storage).
|
|
4109
4481
|
*
|
|
4110
|
-
*
|
|
4482
|
+
* Use this method when handling storage externally (e.g., with Multer + S3).
|
|
4483
|
+
* For integrated upload, use `uploadFile()` instead.
|
|
4484
|
+
*
|
|
4485
|
+
* @param data - File metadata
|
|
4486
|
+
* @returns Created file record
|
|
4487
|
+
*
|
|
4488
|
+
* @example
|
|
4489
|
+
* ```typescript
|
|
4490
|
+
* // After uploading to S3 with Multer
|
|
4111
4491
|
* const file = await service.createFile({
|
|
4112
4492
|
* tenantId: "tenant-123",
|
|
4113
4493
|
* name: "contract-2025.pdf",
|
|
@@ -4129,8 +4509,26 @@ var FileService = class {
|
|
|
4129
4509
|
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
4130
4510
|
);
|
|
4131
4511
|
}
|
|
4132
|
-
|
|
4512
|
+
const file2 = await this.adapter.files.create(data);
|
|
4513
|
+
if (this.auditService && this.userId) {
|
|
4514
|
+
await this.auditService.logFileAction({
|
|
4515
|
+
action: "file.uploaded",
|
|
4516
|
+
actorId: this.userId,
|
|
4517
|
+
actorEmail: this.userEmail,
|
|
4518
|
+
fileId: file2.id,
|
|
4519
|
+
fileName: file2.name,
|
|
4520
|
+
metadata: {
|
|
4521
|
+
mimeType: file2.mimeType,
|
|
4522
|
+
size: file2.size,
|
|
4523
|
+
visibility: file2.visibility
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
}
|
|
4527
|
+
return file2;
|
|
4133
4528
|
}
|
|
4529
|
+
// ============================================================================
|
|
4530
|
+
// READ
|
|
4531
|
+
// ============================================================================
|
|
4134
4532
|
/**
|
|
4135
4533
|
* Get file by ID
|
|
4136
4534
|
*/
|
|
@@ -4151,6 +4549,9 @@ var FileService = class {
|
|
|
4151
4549
|
}
|
|
4152
4550
|
return file2;
|
|
4153
4551
|
}
|
|
4552
|
+
// ============================================================================
|
|
4553
|
+
// UPDATE
|
|
4554
|
+
// ============================================================================
|
|
4154
4555
|
/**
|
|
4155
4556
|
* Update file metadata
|
|
4156
4557
|
*
|
|
@@ -4159,18 +4560,44 @@ var FileService = class {
|
|
|
4159
4560
|
* @returns Updated file
|
|
4160
4561
|
*/
|
|
4161
4562
|
async updateFile(fileId, data) {
|
|
4162
|
-
await this.getFileOrThrow(fileId);
|
|
4163
|
-
|
|
4563
|
+
const existingFile = await this.getFileOrThrow(fileId);
|
|
4564
|
+
const updatedFile = await this.adapter.files.update(fileId, data);
|
|
4565
|
+
if (this.auditService && this.userId) {
|
|
4566
|
+
const changes = [];
|
|
4567
|
+
for (const key of Object.keys(data)) {
|
|
4568
|
+
if (data[key] !== void 0 && data[key] !== existingFile[key]) {
|
|
4569
|
+
changes.push({
|
|
4570
|
+
field: key,
|
|
4571
|
+
oldValue: existingFile[key],
|
|
4572
|
+
newValue: data[key]
|
|
4573
|
+
});
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
if (changes.length > 0) {
|
|
4577
|
+
await this.auditService.logFileAction({
|
|
4578
|
+
action: "file.updated",
|
|
4579
|
+
actorId: this.userId,
|
|
4580
|
+
actorEmail: this.userEmail,
|
|
4581
|
+
fileId,
|
|
4582
|
+
fileName: updatedFile.name,
|
|
4583
|
+
changes
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
return updatedFile;
|
|
4164
4588
|
}
|
|
4589
|
+
// ============================================================================
|
|
4590
|
+
// DELETE
|
|
4591
|
+
// ============================================================================
|
|
4165
4592
|
/**
|
|
4166
|
-
* Delete file (soft delete)
|
|
4593
|
+
* Delete file (soft delete by default)
|
|
4167
4594
|
*
|
|
4168
4595
|
* @param fileId - File UUID
|
|
4169
4596
|
* @param options - Delete options
|
|
4170
4597
|
*/
|
|
4171
4598
|
async deleteFile(fileId, options) {
|
|
4599
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
4172
4600
|
if (options?.checkOwnership && options.userId) {
|
|
4173
|
-
const file2 = await this.getFileOrThrow(fileId);
|
|
4174
4601
|
if (file2.uploadedBy !== options.userId) {
|
|
4175
4602
|
throw new Error("You can only delete files you uploaded");
|
|
4176
4603
|
}
|
|
@@ -4180,7 +4607,84 @@ var FileService = class {
|
|
|
4180
4607
|
} else {
|
|
4181
4608
|
await this.adapter.files.delete(fileId);
|
|
4182
4609
|
}
|
|
4610
|
+
if (this.auditService && this.userId) {
|
|
4611
|
+
await this.auditService.logFileAction({
|
|
4612
|
+
action: "file.deleted",
|
|
4613
|
+
actorId: this.userId,
|
|
4614
|
+
actorEmail: this.userEmail,
|
|
4615
|
+
fileId,
|
|
4616
|
+
fileName: file2.name
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
/**
|
|
4621
|
+
* Delete file from both storage and database.
|
|
4622
|
+
*
|
|
4623
|
+
* Requires `adapter.storage` to be configured.
|
|
4624
|
+
*
|
|
4625
|
+
* @param fileId - File UUID
|
|
4626
|
+
* @param options - Delete options
|
|
4627
|
+
* @throws Error if StorageAdapter is not configured
|
|
4628
|
+
*/
|
|
4629
|
+
async deleteFileWithStorage(fileId, options) {
|
|
4630
|
+
if (!this.adapter.storage) {
|
|
4631
|
+
throw new Error(
|
|
4632
|
+
"StorageAdapter is not configured. Provide adapter.storage to use deleteFileWithStorage()."
|
|
4633
|
+
);
|
|
4634
|
+
}
|
|
4635
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
4636
|
+
await this.adapter.storage.delete(file2.storagePath);
|
|
4637
|
+
if (options?.hard) {
|
|
4638
|
+
await this.adapter.files.hardDelete(fileId);
|
|
4639
|
+
} else {
|
|
4640
|
+
await this.adapter.files.delete(fileId);
|
|
4641
|
+
}
|
|
4642
|
+
if (this.auditService && this.userId) {
|
|
4643
|
+
await this.auditService.logFileAction({
|
|
4644
|
+
action: "file.deleted",
|
|
4645
|
+
actorId: this.userId,
|
|
4646
|
+
actorEmail: this.userEmail,
|
|
4647
|
+
fileId,
|
|
4648
|
+
fileName: file2.name,
|
|
4649
|
+
metadata: { deletedFromStorage: true }
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
/**
|
|
4654
|
+
* Delete multiple files
|
|
4655
|
+
*
|
|
4656
|
+
* @param fileIds - Array of file UUIDs
|
|
4657
|
+
* @param options - Delete options
|
|
4658
|
+
*/
|
|
4659
|
+
async bulkDelete(fileIds, options) {
|
|
4660
|
+
for (const fileId of fileIds) {
|
|
4661
|
+
const file2 = await this.getFile(fileId);
|
|
4662
|
+
if (!file2) {
|
|
4663
|
+
continue;
|
|
4664
|
+
}
|
|
4665
|
+
if (options?.deleteFromStorage && this.adapter.storage) {
|
|
4666
|
+
await this.adapter.storage.delete(file2.storagePath);
|
|
4667
|
+
}
|
|
4668
|
+
if (options?.hard) {
|
|
4669
|
+
await this.adapter.files.hardDelete(fileId);
|
|
4670
|
+
} else {
|
|
4671
|
+
await this.adapter.files.delete(fileId);
|
|
4672
|
+
}
|
|
4673
|
+
if (this.auditService && this.userId) {
|
|
4674
|
+
await this.auditService.logFileAction({
|
|
4675
|
+
action: "file.deleted",
|
|
4676
|
+
actorId: this.userId,
|
|
4677
|
+
actorEmail: this.userEmail,
|
|
4678
|
+
fileId,
|
|
4679
|
+
fileName: file2.name,
|
|
4680
|
+
metadata: { deletedFromStorage: options?.deleteFromStorage ?? false }
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4183
4684
|
}
|
|
4685
|
+
// ============================================================================
|
|
4686
|
+
// LIST
|
|
4687
|
+
// ============================================================================
|
|
4184
4688
|
/**
|
|
4185
4689
|
* List files for the tenant
|
|
4186
4690
|
*/
|
|
@@ -4199,6 +4703,74 @@ var FileService = class {
|
|
|
4199
4703
|
async listFilesByUploader(uploadedBy) {
|
|
4200
4704
|
return await this.adapter.files.findByUploader(uploadedBy);
|
|
4201
4705
|
}
|
|
4706
|
+
// ============================================================================
|
|
4707
|
+
// SIGNED URL
|
|
4708
|
+
// ============================================================================
|
|
4709
|
+
/**
|
|
4710
|
+
* Get a signed URL for private file access.
|
|
4711
|
+
*
|
|
4712
|
+
* Checks access permissions before generating URL.
|
|
4713
|
+
* Requires `adapter.storage` to be configured.
|
|
4714
|
+
*
|
|
4715
|
+
* @param fileId - File UUID
|
|
4716
|
+
* @param userId - User requesting access
|
|
4717
|
+
* @param options - Signed URL options
|
|
4718
|
+
* @returns Signed URL
|
|
4719
|
+
* @throws Error if user doesn't have access or StorageAdapter is not configured
|
|
4720
|
+
*
|
|
4721
|
+
* @example
|
|
4722
|
+
* ```typescript
|
|
4723
|
+
* const url = await service.getSignedUrl("file-123", "user-456", {
|
|
4724
|
+
* expiresIn: 3600, // 1 hour
|
|
4725
|
+
* });
|
|
4726
|
+
* ```
|
|
4727
|
+
*/
|
|
4728
|
+
async getSignedUrl(fileId, userId, options) {
|
|
4729
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
4730
|
+
const hasAccess = await this.checkAccess(fileId, userId);
|
|
4731
|
+
if (!hasAccess) {
|
|
4732
|
+
throw new Error("Access denied to this file");
|
|
4733
|
+
}
|
|
4734
|
+
if (file2.visibility === "public") {
|
|
4735
|
+
return file2.url;
|
|
4736
|
+
}
|
|
4737
|
+
if (!this.adapter.storage) {
|
|
4738
|
+
return file2.url;
|
|
4739
|
+
}
|
|
4740
|
+
return await this.adapter.storage.getSignedUrl(file2.storagePath, options);
|
|
4741
|
+
}
|
|
4742
|
+
// ============================================================================
|
|
4743
|
+
// ACCESS CONTROL
|
|
4744
|
+
// ============================================================================
|
|
4745
|
+
/**
|
|
4746
|
+
* Check if user has access to a file
|
|
4747
|
+
*
|
|
4748
|
+
* @param fileId - File UUID
|
|
4749
|
+
* @param userId - User ID to check
|
|
4750
|
+
* @returns true if user can access the file
|
|
4751
|
+
*/
|
|
4752
|
+
async checkAccess(fileId, userId) {
|
|
4753
|
+
const file2 = await this.getFile(fileId);
|
|
4754
|
+
if (!file2) {
|
|
4755
|
+
return false;
|
|
4756
|
+
}
|
|
4757
|
+
if (file2.visibility === "public") {
|
|
4758
|
+
return true;
|
|
4759
|
+
}
|
|
4760
|
+
if (file2.uploadedBy === userId) {
|
|
4761
|
+
return true;
|
|
4762
|
+
}
|
|
4763
|
+
if (file2.visibility === "restricted") {
|
|
4764
|
+
return file2.allowedUsers?.includes(userId) ?? false;
|
|
4765
|
+
}
|
|
4766
|
+
return false;
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* @deprecated Use checkAccess() instead
|
|
4770
|
+
*/
|
|
4771
|
+
canAccess(fileId, userId) {
|
|
4772
|
+
return this.checkAccess(fileId, userId);
|
|
4773
|
+
}
|
|
4202
4774
|
/**
|
|
4203
4775
|
* Change file visibility
|
|
4204
4776
|
*
|
|
@@ -4242,29 +4814,9 @@ var FileService = class {
|
|
|
4242
4814
|
allowedUsers: newAllowed
|
|
4243
4815
|
});
|
|
4244
4816
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
* @param fileId - File UUID
|
|
4249
|
-
* @param userId - User ID to check
|
|
4250
|
-
* @returns true if user can access the file
|
|
4251
|
-
*/
|
|
4252
|
-
async canAccess(fileId, userId) {
|
|
4253
|
-
const file2 = await this.getFile(fileId);
|
|
4254
|
-
if (!file2) {
|
|
4255
|
-
return false;
|
|
4256
|
-
}
|
|
4257
|
-
if (file2.visibility === "public") {
|
|
4258
|
-
return true;
|
|
4259
|
-
}
|
|
4260
|
-
if (file2.uploadedBy === userId) {
|
|
4261
|
-
return true;
|
|
4262
|
-
}
|
|
4263
|
-
if (file2.visibility === "restricted") {
|
|
4264
|
-
return file2.allowedUsers?.includes(userId) ?? false;
|
|
4265
|
-
}
|
|
4266
|
-
return false;
|
|
4267
|
-
}
|
|
4817
|
+
// ============================================================================
|
|
4818
|
+
// ORGANIZATION
|
|
4819
|
+
// ============================================================================
|
|
4268
4820
|
/**
|
|
4269
4821
|
* Move file to different folder
|
|
4270
4822
|
*/
|
|
@@ -4351,16 +4903,26 @@ var FlowService = class {
|
|
|
4351
4903
|
*/
|
|
4352
4904
|
async createFlow(input, tenantId) {
|
|
4353
4905
|
if (!this.adapter.flows) {
|
|
4354
|
-
throw new
|
|
4906
|
+
throw new SchemaError(
|
|
4907
|
+
"Flows feature is not enabled. Database adapter does not support flows.",
|
|
4908
|
+
SchemaErrorCode.UNKNOWN,
|
|
4909
|
+
{ feature: "flows" }
|
|
4910
|
+
);
|
|
4355
4911
|
}
|
|
4356
4912
|
this.validateFlowName(input.name);
|
|
4357
4913
|
const existing = await this.adapter.flows.findByName(tenantId, input.name);
|
|
4358
4914
|
if (existing) {
|
|
4359
|
-
throw new
|
|
4915
|
+
throw new SchemaError(
|
|
4916
|
+
`Flow "${input.name}" already exists`,
|
|
4917
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
4918
|
+
{ flowName: input.name }
|
|
4919
|
+
);
|
|
4360
4920
|
}
|
|
4361
4921
|
if (this.systemFlows.has(input.name)) {
|
|
4362
|
-
throw new
|
|
4363
|
-
`Cannot create flow "${input.name}": a system flow with this name already exists
|
|
4922
|
+
throw new SchemaError(
|
|
4923
|
+
`Cannot create flow "${input.name}": a system flow with this name already exists`,
|
|
4924
|
+
SchemaErrorCode.DUPLICATE_OBJECT,
|
|
4925
|
+
{ flowName: input.name, system: true }
|
|
4364
4926
|
);
|
|
4365
4927
|
}
|
|
4366
4928
|
this.validateFlowStructure(input);
|
|
@@ -4385,14 +4947,16 @@ var FlowService = class {
|
|
|
4385
4947
|
*/
|
|
4386
4948
|
async updateFlow(flowId, input) {
|
|
4387
4949
|
if (!this.adapter.flows) {
|
|
4388
|
-
throw new
|
|
4950
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
4951
|
+
feature: "flows"
|
|
4952
|
+
});
|
|
4389
4953
|
}
|
|
4390
4954
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4391
4955
|
if (!dbFlow) {
|
|
4392
|
-
throw new
|
|
4956
|
+
throw new NotFoundError("Flow", flowId);
|
|
4393
4957
|
}
|
|
4394
4958
|
if (dbFlow.system) {
|
|
4395
|
-
throw new
|
|
4959
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
4396
4960
|
}
|
|
4397
4961
|
if (input.slots || input.pages || input.relations) {
|
|
4398
4962
|
this.validateFlowStructure({
|
|
@@ -4419,14 +4983,16 @@ var FlowService = class {
|
|
|
4419
4983
|
*/
|
|
4420
4984
|
async publishFlow(flowId) {
|
|
4421
4985
|
if (!this.adapter.flows) {
|
|
4422
|
-
throw new
|
|
4986
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
4987
|
+
feature: "flows"
|
|
4988
|
+
});
|
|
4423
4989
|
}
|
|
4424
4990
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4425
4991
|
if (!dbFlow) {
|
|
4426
|
-
throw new
|
|
4992
|
+
throw new NotFoundError("Flow", flowId);
|
|
4427
4993
|
}
|
|
4428
4994
|
if (dbFlow.system) {
|
|
4429
|
-
throw new
|
|
4995
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
4430
4996
|
}
|
|
4431
4997
|
if (dbFlow.status === "published") {
|
|
4432
4998
|
return this.convertDBFlowToDefinition(dbFlow);
|
|
@@ -4449,14 +5015,16 @@ var FlowService = class {
|
|
|
4449
5015
|
*/
|
|
4450
5016
|
async archiveFlow(flowId) {
|
|
4451
5017
|
if (!this.adapter.flows) {
|
|
4452
|
-
throw new
|
|
5018
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
5019
|
+
feature: "flows"
|
|
5020
|
+
});
|
|
4453
5021
|
}
|
|
4454
5022
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4455
5023
|
if (!dbFlow) {
|
|
4456
|
-
throw new
|
|
5024
|
+
throw new NotFoundError("Flow", flowId);
|
|
4457
5025
|
}
|
|
4458
5026
|
if (dbFlow.system) {
|
|
4459
|
-
throw new
|
|
5027
|
+
throw new ProtectedResourceError("object", dbFlow.name, "modify");
|
|
4460
5028
|
}
|
|
4461
5029
|
const updated = await this.adapter.flows.update(flowId, {
|
|
4462
5030
|
status: "archived"
|
|
@@ -4468,14 +5036,16 @@ var FlowService = class {
|
|
|
4468
5036
|
*/
|
|
4469
5037
|
async deleteFlow(flowId) {
|
|
4470
5038
|
if (!this.adapter.flows) {
|
|
4471
|
-
throw new
|
|
5039
|
+
throw new SchemaError("Flows feature is not enabled.", SchemaErrorCode.UNKNOWN, {
|
|
5040
|
+
feature: "flows"
|
|
5041
|
+
});
|
|
4472
5042
|
}
|
|
4473
5043
|
const dbFlow = await this.adapter.flows.findById(flowId);
|
|
4474
5044
|
if (!dbFlow) {
|
|
4475
|
-
throw new
|
|
5045
|
+
throw new NotFoundError("Flow", flowId);
|
|
4476
5046
|
}
|
|
4477
5047
|
if (dbFlow.system) {
|
|
4478
|
-
throw new
|
|
5048
|
+
throw new ProtectedResourceError("object", dbFlow.name, "delete");
|
|
4479
5049
|
}
|
|
4480
5050
|
await this.adapter.flows.delete(flowId);
|
|
4481
5051
|
}
|
|
@@ -4487,16 +5057,23 @@ var FlowService = class {
|
|
|
4487
5057
|
*/
|
|
4488
5058
|
validateFlowName(name) {
|
|
4489
5059
|
if (!name || name.length === 0) {
|
|
4490
|
-
throw new
|
|
5060
|
+
throw new ValidationError("Flow name cannot be empty", [
|
|
5061
|
+
{ path: ["name"], message: "Flow name cannot be empty" }
|
|
5062
|
+
]);
|
|
4491
5063
|
}
|
|
4492
5064
|
if (name.length > 63) {
|
|
4493
|
-
throw new
|
|
5065
|
+
throw new ValidationError("Flow name is too long", [
|
|
5066
|
+
{ path: ["name"], message: "Flow name is too long (max 63 characters)" }
|
|
5067
|
+
]);
|
|
4494
5068
|
}
|
|
4495
5069
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
4496
5070
|
if (!kebabCaseRegex.test(name)) {
|
|
4497
|
-
throw new
|
|
4498
|
-
|
|
4499
|
-
|
|
5071
|
+
throw new ValidationError("Invalid flow name format", [
|
|
5072
|
+
{
|
|
5073
|
+
path: ["name"],
|
|
5074
|
+
message: "Flow name must be in kebab-case (e.g., 'couple-creation', 'new-contact')"
|
|
5075
|
+
}
|
|
5076
|
+
]);
|
|
4500
5077
|
}
|
|
4501
5078
|
}
|
|
4502
5079
|
/**
|
|
@@ -4504,33 +5081,59 @@ var FlowService = class {
|
|
|
4504
5081
|
*/
|
|
4505
5082
|
validateFlowStructure(input) {
|
|
4506
5083
|
if (!input.slots || input.slots.length === 0) {
|
|
4507
|
-
throw new
|
|
5084
|
+
throw new ValidationError("Flow must have at least one slot", [
|
|
5085
|
+
{ path: ["slots"], message: "Flow must have at least one slot" }
|
|
5086
|
+
]);
|
|
4508
5087
|
}
|
|
4509
5088
|
if (!input.pages || input.pages.length === 0) {
|
|
4510
|
-
throw new
|
|
5089
|
+
throw new ValidationError("Flow must have at least one page", [
|
|
5090
|
+
{ path: ["pages"], message: "Flow must have at least one page" }
|
|
5091
|
+
]);
|
|
4511
5092
|
}
|
|
4512
5093
|
const slotIds = new Set(input.slots.map((s) => s.id));
|
|
4513
5094
|
if (slotIds.size !== input.slots.length) {
|
|
4514
|
-
throw new
|
|
5095
|
+
throw new ValidationError("Duplicate slot IDs detected", [
|
|
5096
|
+
{ path: ["slots"], message: "Duplicate slot IDs detected" }
|
|
5097
|
+
]);
|
|
4515
5098
|
}
|
|
4516
5099
|
for (const page of input.pages) {
|
|
4517
5100
|
for (const row of page.rows) {
|
|
4518
5101
|
for (const field of row.fields) {
|
|
4519
5102
|
if (!slotIds.has(field.slotId)) {
|
|
4520
|
-
throw new
|
|
5103
|
+
throw new ValidationError("Field references unknown slot", [
|
|
5104
|
+
{
|
|
5105
|
+
path: ["pages", page.id, "rows", row.id, "fields", field.id],
|
|
5106
|
+
message: `Field "${field.id}" references unknown slot "${field.slotId}"`
|
|
5107
|
+
}
|
|
5108
|
+
]);
|
|
4521
5109
|
}
|
|
4522
5110
|
}
|
|
4523
5111
|
}
|
|
4524
5112
|
}
|
|
4525
5113
|
for (const relation2 of input.relations) {
|
|
4526
5114
|
if (!slotIds.has(relation2.sourceSlotId)) {
|
|
4527
|
-
throw new
|
|
5115
|
+
throw new ValidationError("Relation references unknown source slot", [
|
|
5116
|
+
{
|
|
5117
|
+
path: ["relations", relation2.id, "sourceSlotId"],
|
|
5118
|
+
message: `Relation references unknown source slot "${relation2.sourceSlotId}"`
|
|
5119
|
+
}
|
|
5120
|
+
]);
|
|
4528
5121
|
}
|
|
4529
5122
|
if (!slotIds.has(relation2.targetSlotId)) {
|
|
4530
|
-
throw new
|
|
5123
|
+
throw new ValidationError("Relation references unknown target slot", [
|
|
5124
|
+
{
|
|
5125
|
+
path: ["relations", relation2.id, "targetSlotId"],
|
|
5126
|
+
message: `Relation references unknown target slot "${relation2.targetSlotId}"`
|
|
5127
|
+
}
|
|
5128
|
+
]);
|
|
4531
5129
|
}
|
|
4532
5130
|
if (relation2.sourceSlotId === relation2.targetSlotId) {
|
|
4533
|
-
throw new
|
|
5131
|
+
throw new ValidationError("Relation cannot link a slot to itself", [
|
|
5132
|
+
{
|
|
5133
|
+
path: ["relations", relation2.id],
|
|
5134
|
+
message: `Relation cannot link a slot to itself: "${relation2.sourceSlotId}"`
|
|
5135
|
+
}
|
|
5136
|
+
]);
|
|
4534
5137
|
}
|
|
4535
5138
|
}
|
|
4536
5139
|
}
|
|
@@ -5023,14 +5626,15 @@ var ObjectSchemaService = class {
|
|
|
5023
5626
|
});
|
|
5024
5627
|
}
|
|
5025
5628
|
}
|
|
5629
|
+
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
5630
|
+
const attributes = dbAttributes.map((attr) => this.convertDBAttributeToAttribute(attr));
|
|
5026
5631
|
if (updates.labelExpression !== void 0 && updates.labelExpression !== oldValues.labelExpression) {
|
|
5027
5632
|
const newExpression = updates.labelExpression;
|
|
5028
|
-
await this.adapter.objectRecords.batchRefreshLabels(
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
);
|
|
5633
|
+
await this.adapter.objectRecords.batchRefreshLabels(objectId, (values) => {
|
|
5634
|
+
const enrichedValues = enrichValuesWithSelectLabels(values, attributes);
|
|
5635
|
+
return renderLabelExpression(newExpression, enrichedValues);
|
|
5636
|
+
});
|
|
5032
5637
|
}
|
|
5033
|
-
const dbAttributes = await this.adapter.attributes.findByObjectId(objectId);
|
|
5034
5638
|
return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
|
|
5035
5639
|
}
|
|
5036
5640
|
/**
|
|
@@ -5349,6 +5953,8 @@ var PermissionService = class {
|
|
|
5349
5953
|
this.adapter = adapter;
|
|
5350
5954
|
this.tenantId = tenantId;
|
|
5351
5955
|
this.cache = /* @__PURE__ */ new Map();
|
|
5956
|
+
/** Track pending permission fetches to prevent duplicate concurrent requests */
|
|
5957
|
+
this.pendingFetches = /* @__PURE__ */ new Map();
|
|
5352
5958
|
if (!adapter.permissions) {
|
|
5353
5959
|
throw new Error(
|
|
5354
5960
|
"PermissionService requires a DatabaseAdapter with permissions repository. Make sure your adapter implements the permissions property."
|
|
@@ -5497,6 +6103,23 @@ var PermissionService = class {
|
|
|
5497
6103
|
if (cached && cached.expiresAt > Date.now()) {
|
|
5498
6104
|
return cached.permissions;
|
|
5499
6105
|
}
|
|
6106
|
+
const pendingFetch = this.pendingFetches.get(cacheKey);
|
|
6107
|
+
if (pendingFetch) {
|
|
6108
|
+
return pendingFetch;
|
|
6109
|
+
}
|
|
6110
|
+
const fetchPromise = this.fetchAndCachePermissions(userProfileId, cacheKey);
|
|
6111
|
+
this.pendingFetches.set(cacheKey, fetchPromise);
|
|
6112
|
+
try {
|
|
6113
|
+
return await fetchPromise;
|
|
6114
|
+
} finally {
|
|
6115
|
+
this.pendingFetches.delete(cacheKey);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
/**
|
|
6119
|
+
* Fetch permissions from database and cache the result
|
|
6120
|
+
* @internal
|
|
6121
|
+
*/
|
|
6122
|
+
async fetchAndCachePermissions(userProfileId, cacheKey) {
|
|
5500
6123
|
const permissions = await this.permissionsRepo.getEffectivePermissions(
|
|
5501
6124
|
userProfileId,
|
|
5502
6125
|
this.tenantId
|
|
@@ -5879,8 +6502,11 @@ var RelationService = class {
|
|
|
5879
6502
|
async validateRelationsOrThrow(schema, data) {
|
|
5880
6503
|
const result = await this.validateRelations(schema, data);
|
|
5881
6504
|
if (!result.valid) {
|
|
5882
|
-
const
|
|
5883
|
-
|
|
6505
|
+
const errors = result.errors.map((e) => ({
|
|
6506
|
+
path: [e.attribute],
|
|
6507
|
+
message: e.message
|
|
6508
|
+
}));
|
|
6509
|
+
throw new ValidationError("Relation validation failed", errors);
|
|
5884
6510
|
}
|
|
5885
6511
|
}
|
|
5886
6512
|
// ============================================================================
|
|
@@ -5928,7 +6554,8 @@ var RelationService = class {
|
|
|
5928
6554
|
totalCount += result.total;
|
|
5929
6555
|
for (const record of result.records) {
|
|
5930
6556
|
const template = target.displayTemplate || objectSchema.labelExpression;
|
|
5931
|
-
const
|
|
6557
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
6558
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
5932
6559
|
allOptions.push({
|
|
5933
6560
|
id: record.id,
|
|
5934
6561
|
objectId: objectSchema.id,
|
|
@@ -5992,7 +6619,8 @@ var RelationService = class {
|
|
|
5992
6619
|
}
|
|
5993
6620
|
}
|
|
5994
6621
|
for (const record of objectRecords) {
|
|
5995
|
-
const
|
|
6622
|
+
const enrichedValues = enrichValuesWithSelectLabels(record.values, objectSchema.attributes);
|
|
6623
|
+
const label = renderLabelExpression(template, enrichedValues);
|
|
5996
6624
|
resolved.push({
|
|
5997
6625
|
id: record.id,
|
|
5998
6626
|
objectId: record.objectId,
|
|
@@ -6013,10 +6641,131 @@ var RelationService = class {
|
|
|
6013
6641
|
if (!attribute || attribute.type !== "relation") {
|
|
6014
6642
|
return null;
|
|
6015
6643
|
}
|
|
6016
|
-
|
|
6644
|
+
const merged = {
|
|
6017
6645
|
...attribute.config,
|
|
6018
6646
|
...attribute
|
|
6019
6647
|
};
|
|
6648
|
+
if (!("targets" in merged && Array.isArray(merged.targets) && "cardinality" in merged)) {
|
|
6649
|
+
return null;
|
|
6650
|
+
}
|
|
6651
|
+
return merged;
|
|
6652
|
+
}
|
|
6653
|
+
};
|
|
6654
|
+
|
|
6655
|
+
// src/runtime/services/user.service.ts
|
|
6656
|
+
var UserService = class {
|
|
6657
|
+
constructor(adapter, tenantId) {
|
|
6658
|
+
this.adapter = adapter;
|
|
6659
|
+
this.tenantId = tenantId;
|
|
6660
|
+
}
|
|
6661
|
+
/**
|
|
6662
|
+
* Validate all user attributes in the data
|
|
6663
|
+
*
|
|
6664
|
+
* @param schema - Object schema containing attribute definitions
|
|
6665
|
+
* @param data - Record data to validate
|
|
6666
|
+
* @returns Validation result with errors if any
|
|
6667
|
+
*
|
|
6668
|
+
* @example
|
|
6669
|
+
* ```typescript
|
|
6670
|
+
* const result = await userService.validateUsers(schema, {
|
|
6671
|
+
* assignee: "user-123",
|
|
6672
|
+
* watchers: ["user-456", "user-789"]
|
|
6673
|
+
* });
|
|
6674
|
+
*
|
|
6675
|
+
* if (!result.valid) {
|
|
6676
|
+
* console.log(result.errors);
|
|
6677
|
+
* // [{ attribute: "assignee", message: "User not found", invalidIds: ["user-123"] }]
|
|
6678
|
+
* }
|
|
6679
|
+
* ```
|
|
6680
|
+
*/
|
|
6681
|
+
async validateUsers(schema, data) {
|
|
6682
|
+
const errors = [];
|
|
6683
|
+
const userAttrs = schema.attributes.filter(
|
|
6684
|
+
(attr) => attr.type === "user"
|
|
6685
|
+
);
|
|
6686
|
+
for (const attr of userAttrs) {
|
|
6687
|
+
const value = data[attr.name];
|
|
6688
|
+
if (value === void 0 || value === null) {
|
|
6689
|
+
continue;
|
|
6690
|
+
}
|
|
6691
|
+
const attrErrors = await this.validateUserAttribute(attr, value);
|
|
6692
|
+
errors.push(...attrErrors);
|
|
6693
|
+
}
|
|
6694
|
+
return {
|
|
6695
|
+
valid: errors.length === 0,
|
|
6696
|
+
errors
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
/**
|
|
6700
|
+
* Validate a single user attribute value
|
|
6701
|
+
*/
|
|
6702
|
+
async validateUserAttribute(attr, value) {
|
|
6703
|
+
const errors = [];
|
|
6704
|
+
const ids = this.extractIds(attr, value);
|
|
6705
|
+
if (ids.length === 0) {
|
|
6706
|
+
return errors;
|
|
6707
|
+
}
|
|
6708
|
+
const invalidIds = [];
|
|
6709
|
+
const roleErrors = [];
|
|
6710
|
+
for (const id of ids) {
|
|
6711
|
+
const user2 = await this.adapter.userProfiles.findById(id);
|
|
6712
|
+
if (!user2) {
|
|
6713
|
+
invalidIds.push(id);
|
|
6714
|
+
continue;
|
|
6715
|
+
}
|
|
6716
|
+
if (user2.tenantId !== this.tenantId) {
|
|
6717
|
+
invalidIds.push(id);
|
|
6718
|
+
continue;
|
|
6719
|
+
}
|
|
6720
|
+
if (attr.allowedRoles && attr.allowedRoles.length > 0) {
|
|
6721
|
+
if (!attr.allowedRoles.includes(user2.role)) {
|
|
6722
|
+
roleErrors.push(id);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
if (invalidIds.length > 0) {
|
|
6727
|
+
errors.push({
|
|
6728
|
+
attribute: attr.name,
|
|
6729
|
+
message: `Invalid or non-existent users for ${attr.label}`,
|
|
6730
|
+
invalidIds
|
|
6731
|
+
});
|
|
6732
|
+
}
|
|
6733
|
+
if (roleErrors.length > 0) {
|
|
6734
|
+
errors.push({
|
|
6735
|
+
attribute: attr.name,
|
|
6736
|
+
message: `Users do not have required role for ${attr.label}. Allowed roles: ${attr.allowedRoles?.join(", ")}`,
|
|
6737
|
+
invalidIds: roleErrors
|
|
6738
|
+
});
|
|
6739
|
+
}
|
|
6740
|
+
return errors;
|
|
6741
|
+
}
|
|
6742
|
+
/**
|
|
6743
|
+
* Extract IDs from user value based on multiple flag
|
|
6744
|
+
*/
|
|
6745
|
+
extractIds(attr, value) {
|
|
6746
|
+
if (attr.multiple) {
|
|
6747
|
+
if (!Array.isArray(value)) {
|
|
6748
|
+
return [];
|
|
6749
|
+
}
|
|
6750
|
+
return value.filter((v) => typeof v === "string" && v !== "");
|
|
6751
|
+
}
|
|
6752
|
+
if (typeof value === "string" && value !== "") {
|
|
6753
|
+
return [value];
|
|
6754
|
+
}
|
|
6755
|
+
return [];
|
|
6756
|
+
}
|
|
6757
|
+
/**
|
|
6758
|
+
* Validate users and throw if invalid
|
|
6759
|
+
*/
|
|
6760
|
+
async validateUsersOrThrow(schema, data) {
|
|
6761
|
+
const result = await this.validateUsers(schema, data);
|
|
6762
|
+
if (!result.valid) {
|
|
6763
|
+
const errors = result.errors.map((e) => ({
|
|
6764
|
+
path: [e.attribute],
|
|
6765
|
+
message: e.message
|
|
6766
|
+
}));
|
|
6767
|
+
throw new ValidationError("User validation failed", errors);
|
|
6768
|
+
}
|
|
6020
6769
|
}
|
|
6021
6770
|
};
|
|
6022
6771
|
|
|
@@ -6027,9 +6776,10 @@ var RecordService = class {
|
|
|
6027
6776
|
this.tenantId = tenantId;
|
|
6028
6777
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
6029
6778
|
this.relationService = new RelationService(adapter, registry);
|
|
6779
|
+
this.userService = new UserService(adapter, tenantId);
|
|
6030
6780
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
6031
6781
|
this.permissionService = options?.permissionService;
|
|
6032
|
-
this.auditService = options?.auditService;
|
|
6782
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
6033
6783
|
this.userId = options?.userId;
|
|
6034
6784
|
this.userEmail = options?.userEmail;
|
|
6035
6785
|
}
|
|
@@ -6096,21 +6846,23 @@ var RecordService = class {
|
|
|
6096
6846
|
/**
|
|
6097
6847
|
* Compute display label from schema expression
|
|
6098
6848
|
* Automatically resolves relation attribute values to their labels
|
|
6849
|
+
* and select/multiselect values to their option labels
|
|
6099
6850
|
* @internal
|
|
6100
6851
|
*/
|
|
6101
6852
|
async computeLabel(schema, values) {
|
|
6102
6853
|
const attrNames = extractAttributeNames(schema.labelExpression);
|
|
6854
|
+
let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
|
|
6103
6855
|
const relationAttrs = schema.attributes.filter(
|
|
6104
6856
|
(attr) => attr.type === "relation" && attrNames.includes(attr.name)
|
|
6105
6857
|
);
|
|
6106
6858
|
if (relationAttrs.length === 0) {
|
|
6107
|
-
return renderLabelExpression(schema.labelExpression,
|
|
6859
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
6108
6860
|
}
|
|
6109
6861
|
const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
|
|
6110
6862
|
if (resolvedMap.size === 0) {
|
|
6111
|
-
return renderLabelExpression(schema.labelExpression,
|
|
6863
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
6112
6864
|
}
|
|
6113
|
-
|
|
6865
|
+
enrichedValues = { ...enrichedValues };
|
|
6114
6866
|
for (const attr of relationAttrs) {
|
|
6115
6867
|
const val = values[attr.name];
|
|
6116
6868
|
const ids = this.extractRelationIds(val);
|
|
@@ -6165,6 +6917,9 @@ var RecordService = class {
|
|
|
6165
6917
|
if (!options?.skipRelationValidation) {
|
|
6166
6918
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6167
6919
|
}
|
|
6920
|
+
if (!options?.skipUserValidation) {
|
|
6921
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
6922
|
+
}
|
|
6168
6923
|
}
|
|
6169
6924
|
const completionStatus = computeRecordStatus(schema, data);
|
|
6170
6925
|
const label = await this.computeLabel(schema, data);
|
|
@@ -6223,7 +6978,7 @@ var RecordService = class {
|
|
|
6223
6978
|
async getRecordOrThrow(recordId) {
|
|
6224
6979
|
const record = await this.getRecord(recordId);
|
|
6225
6980
|
if (!record) {
|
|
6226
|
-
throw new
|
|
6981
|
+
throw new RecordNotFoundError(recordId);
|
|
6227
6982
|
}
|
|
6228
6983
|
return record;
|
|
6229
6984
|
}
|
|
@@ -6276,6 +7031,9 @@ var RecordService = class {
|
|
|
6276
7031
|
if (!options?.skipRelationValidation) {
|
|
6277
7032
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6278
7033
|
}
|
|
7034
|
+
if (!options?.skipUserValidation) {
|
|
7035
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
7036
|
+
}
|
|
6279
7037
|
}
|
|
6280
7038
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
6281
7039
|
const label = await this.computeLabel(schema, mergedData);
|
|
@@ -6401,7 +7159,7 @@ var RecordService = class {
|
|
|
6401
7159
|
await this.checkPermission(schema.name, "delete");
|
|
6402
7160
|
if (options?.checkSystem) {
|
|
6403
7161
|
if (schema.system) {
|
|
6404
|
-
throw new
|
|
7162
|
+
throw new ProtectedResourceError("object", schema.name, "delete");
|
|
6405
7163
|
}
|
|
6406
7164
|
}
|
|
6407
7165
|
if (!options?.skipReferenceCheck) {
|
|
@@ -6450,7 +7208,11 @@ var RecordService = class {
|
|
|
6450
7208
|
async restoreRecord(recordId, options) {
|
|
6451
7209
|
const record = await this.getRecordOrThrow(recordId);
|
|
6452
7210
|
if (!record.deletedAt) {
|
|
6453
|
-
throw new
|
|
7211
|
+
throw new SchemaError(
|
|
7212
|
+
`Record "${recordId}" is not deleted`,
|
|
7213
|
+
SchemaErrorCode.VALIDATION_FAILED,
|
|
7214
|
+
{ recordId, reason: "not_deleted" }
|
|
7215
|
+
);
|
|
6454
7216
|
}
|
|
6455
7217
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
6456
7218
|
await this.checkPermission(schema.name, "update");
|
|
@@ -6888,11 +7650,17 @@ var ViewService = class {
|
|
|
6888
7650
|
this.validateViewName(input.name);
|
|
6889
7651
|
const existing = await this.adapter.views.findByName(tenantId, input.objectName, input.name);
|
|
6890
7652
|
if (existing) {
|
|
6891
|
-
throw new
|
|
7653
|
+
throw new SchemaError(
|
|
7654
|
+
`View "${input.name}" already exists for object "${input.objectName}"`,
|
|
7655
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
7656
|
+
{ viewName: input.name, objectName: input.objectName }
|
|
7657
|
+
);
|
|
6892
7658
|
}
|
|
6893
7659
|
if (this.nativeViews.has(input.objectName, input.name)) {
|
|
6894
|
-
throw new
|
|
6895
|
-
`Cannot create view "${input.name}": a system view with this name already exists
|
|
7660
|
+
throw new SchemaError(
|
|
7661
|
+
`Cannot create view "${input.name}": a system view with this name already exists`,
|
|
7662
|
+
SchemaErrorCode.DUPLICATE_ATTRIBUTE,
|
|
7663
|
+
{ viewName: input.name, objectName: input.objectName, system: true }
|
|
6896
7664
|
);
|
|
6897
7665
|
}
|
|
6898
7666
|
const dbView = await this.adapter.views.create({
|
|
@@ -6920,10 +7688,10 @@ var ViewService = class {
|
|
|
6920
7688
|
async updateView(viewId, input) {
|
|
6921
7689
|
const dbView = await this.adapter.views.findById(viewId);
|
|
6922
7690
|
if (!dbView) {
|
|
6923
|
-
throw new
|
|
7691
|
+
throw new NotFoundError("View", viewId);
|
|
6924
7692
|
}
|
|
6925
7693
|
if (dbView.system) {
|
|
6926
|
-
throw new
|
|
7694
|
+
throw new ProtectedResourceError("view", dbView.name, "modify");
|
|
6927
7695
|
}
|
|
6928
7696
|
const updated = await this.adapter.views.update(viewId, {
|
|
6929
7697
|
label: input.label,
|
|
@@ -6943,10 +7711,10 @@ var ViewService = class {
|
|
|
6943
7711
|
async deleteView(viewId) {
|
|
6944
7712
|
const dbView = await this.adapter.views.findById(viewId);
|
|
6945
7713
|
if (!dbView) {
|
|
6946
|
-
throw new
|
|
7714
|
+
throw new NotFoundError("View", viewId);
|
|
6947
7715
|
}
|
|
6948
7716
|
if (dbView.system) {
|
|
6949
|
-
throw new
|
|
7717
|
+
throw new ProtectedResourceError("view", dbView.name, "delete");
|
|
6950
7718
|
}
|
|
6951
7719
|
await this.adapter.views.delete(viewId);
|
|
6952
7720
|
}
|
|
@@ -6960,7 +7728,7 @@ var ViewService = class {
|
|
|
6960
7728
|
async setDefaultView(viewId, tenantId) {
|
|
6961
7729
|
const dbView = await this.adapter.views.findById(viewId);
|
|
6962
7730
|
if (!dbView) {
|
|
6963
|
-
throw new
|
|
7731
|
+
throw new NotFoundError("View", viewId);
|
|
6964
7732
|
}
|
|
6965
7733
|
const currentViews = await this.adapter.views.findByObjectName(tenantId, dbView.objectName);
|
|
6966
7734
|
for (const v of currentViews) {
|
|
@@ -6979,16 +7747,23 @@ var ViewService = class {
|
|
|
6979
7747
|
*/
|
|
6980
7748
|
validateViewName(name) {
|
|
6981
7749
|
if (!name || name.length === 0) {
|
|
6982
|
-
throw new
|
|
7750
|
+
throw new ValidationError("View name cannot be empty", [
|
|
7751
|
+
{ path: ["name"], message: "View name cannot be empty" }
|
|
7752
|
+
]);
|
|
6983
7753
|
}
|
|
6984
7754
|
if (name.length > 63) {
|
|
6985
|
-
throw new
|
|
7755
|
+
throw new ValidationError("View name is too long", [
|
|
7756
|
+
{ path: ["name"], message: "View name is too long (max 63 characters)" }
|
|
7757
|
+
]);
|
|
6986
7758
|
}
|
|
6987
7759
|
const kebabCaseRegex = /^[a-z][a-z0-9-]*$/;
|
|
6988
7760
|
if (!kebabCaseRegex.test(name)) {
|
|
6989
|
-
throw new
|
|
6990
|
-
|
|
6991
|
-
|
|
7761
|
+
throw new ValidationError("Invalid view name format", [
|
|
7762
|
+
{
|
|
7763
|
+
path: ["name"],
|
|
7764
|
+
message: "View name must be in kebab-case (e.g., 'detail', 'list-view')"
|
|
7765
|
+
}
|
|
7766
|
+
]);
|
|
6992
7767
|
}
|
|
6993
7768
|
}
|
|
6994
7769
|
/**
|
|
@@ -7350,6 +8125,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7350
8125
|
DEFAULT_ROLE_LABELS,
|
|
7351
8126
|
DEFAULT_ROLE_PERMISSIONS,
|
|
7352
8127
|
DuplicateError,
|
|
8128
|
+
EMPTY_VALUE_PLACEHOLDER,
|
|
7353
8129
|
FileNotFoundError,
|
|
7354
8130
|
FileService,
|
|
7355
8131
|
FlowBuilder,
|
|
@@ -7387,6 +8163,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7387
8163
|
TableTabConfig,
|
|
7388
8164
|
UserProfileNotFoundError,
|
|
7389
8165
|
UserProfileService,
|
|
8166
|
+
UserService,
|
|
7390
8167
|
ValidationError,
|
|
7391
8168
|
ViewBuilder,
|
|
7392
8169
|
ViewService,
|
|
@@ -7420,10 +8197,12 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7420
8197
|
currencyConfigSchema,
|
|
7421
8198
|
date,
|
|
7422
8199
|
dateConfigSchema,
|
|
8200
|
+
enrichValuesWithSelectLabels,
|
|
7423
8201
|
extractAttributeNames,
|
|
7424
8202
|
file,
|
|
7425
8203
|
fileConfigSchema,
|
|
7426
8204
|
flow,
|
|
8205
|
+
formatAttributeValue,
|
|
7427
8206
|
generateId,
|
|
7428
8207
|
generatePrefixedId,
|
|
7429
8208
|
getAttributeConfigSchema,
|
|
@@ -7431,6 +8210,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7431
8210
|
getSyncPreview,
|
|
7432
8211
|
getViewSyncPreview,
|
|
7433
8212
|
group,
|
|
8213
|
+
isActivityTab,
|
|
7434
8214
|
isAdvancedFilterState,
|
|
7435
8215
|
isCustomTab,
|
|
7436
8216
|
isDefaultRole,
|