@stndrds/schema 0.1.0-alpha.17 → 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-4D476S25.mjs +7713 -0
- package/dist/chunk-CWPOGEK2.mjs +7707 -0
- package/dist/chunk-EWNSOD22.mjs +7707 -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-V5QXU2OK.mjs +82 -0
- package/dist/chunk-XMGMXAHW.mjs +7706 -0
- package/dist/default-roles-AKWIWTLE.mjs +20 -0
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +494 -47
- package/dist/index.mjs +4 -2
- 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 +480 -47
- package/dist/runtime.mjs +3 -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 */
|
|
@@ -167,6 +171,7 @@ __export(index_exports, {
|
|
|
167
171
|
TableTabConfig: () => TableTabConfig,
|
|
168
172
|
UserProfileNotFoundError: () => UserProfileNotFoundError,
|
|
169
173
|
UserProfileService: () => UserProfileService,
|
|
174
|
+
UserService: () => UserService,
|
|
170
175
|
ValidationError: () => ValidationError,
|
|
171
176
|
ViewBuilder: () => ViewBuilder,
|
|
172
177
|
ViewService: () => ViewService,
|
|
@@ -469,6 +474,13 @@ function formatCheckbox(value) {
|
|
|
469
474
|
function formatNumber(value, attribute) {
|
|
470
475
|
if (typeof value !== "number") return String(value);
|
|
471
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
|
+
}
|
|
472
484
|
return value.toLocaleString(void 0, {
|
|
473
485
|
minimumFractionDigits: decimals,
|
|
474
486
|
maximumFractionDigits: decimals
|
|
@@ -640,6 +652,7 @@ var SchemaErrorCode = {
|
|
|
640
652
|
// Protected Resources
|
|
641
653
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
642
654
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
655
|
+
PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
|
|
643
656
|
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
644
657
|
// Permissions
|
|
645
658
|
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
@@ -728,7 +741,7 @@ var ValidationError = class _ValidationError extends SchemaError {
|
|
|
728
741
|
};
|
|
729
742
|
var ProtectedResourceError = class extends SchemaError {
|
|
730
743
|
constructor(resourceType, resourceName, operation) {
|
|
731
|
-
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;
|
|
732
745
|
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
733
746
|
resourceType,
|
|
734
747
|
resourceName,
|
|
@@ -1324,6 +1337,13 @@ var FileAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1324
1337
|
this.attr.verification = config;
|
|
1325
1338
|
return this;
|
|
1326
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Allow multiple files to be uploaded
|
|
1342
|
+
*/
|
|
1343
|
+
multiple() {
|
|
1344
|
+
this.attr.multiple = true;
|
|
1345
|
+
return this;
|
|
1346
|
+
}
|
|
1327
1347
|
required() {
|
|
1328
1348
|
this.setRequired(true);
|
|
1329
1349
|
return this;
|
|
@@ -1344,6 +1364,13 @@ var UserAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1344
1364
|
this.attr.allowedRoles = roles;
|
|
1345
1365
|
return this;
|
|
1346
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Allow multiple users to be selected
|
|
1369
|
+
*/
|
|
1370
|
+
multiple() {
|
|
1371
|
+
this.attr.multiple = true;
|
|
1372
|
+
return this;
|
|
1373
|
+
}
|
|
1347
1374
|
required() {
|
|
1348
1375
|
this.setRequired(true);
|
|
1349
1376
|
return this;
|
|
@@ -2750,10 +2777,12 @@ var fileConfigSchema = baseConfigSchema.extend({
|
|
|
2750
2777
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2751
2778
|
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2752
2779
|
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2753
|
-
verification: fileVerificationConfigSchema.optional()
|
|
2780
|
+
verification: fileVerificationConfigSchema.optional(),
|
|
2781
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2754
2782
|
});
|
|
2755
2783
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2756
|
-
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()
|
|
2757
2786
|
});
|
|
2758
2787
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2759
2788
|
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
@@ -2891,20 +2920,39 @@ function createLocationValidator(_attr) {
|
|
|
2891
2920
|
function createTimestampValidator(_attr) {
|
|
2892
2921
|
return import_zod4.z.number().int().positive();
|
|
2893
2922
|
}
|
|
2894
|
-
function createFileValidator(
|
|
2895
|
-
|
|
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;
|
|
2896
2938
|
}
|
|
2897
|
-
function createUserValidator(
|
|
2898
|
-
|
|
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;
|
|
2899
2947
|
}
|
|
2900
2948
|
function createSingleRelationValidator(attr) {
|
|
2901
|
-
const uuidSchema = import_zod4.z.
|
|
2949
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2902
2950
|
message: `${attr.label} must be a valid record ID`
|
|
2903
2951
|
});
|
|
2904
2952
|
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2905
2953
|
}
|
|
2906
2954
|
function createMultiRelationValidator(attr) {
|
|
2907
|
-
const uuidSchema = import_zod4.z.
|
|
2955
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2908
2956
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2909
2957
|
});
|
|
2910
2958
|
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
@@ -4174,6 +4222,24 @@ var AuditService = class {
|
|
|
4174
4222
|
};
|
|
4175
4223
|
await this.log(entry);
|
|
4176
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
|
+
}
|
|
4177
4243
|
/**
|
|
4178
4244
|
* Log a system action (no actor)
|
|
4179
4245
|
*/
|
|
@@ -4323,21 +4389,105 @@ var AuditService = class {
|
|
|
4323
4389
|
|
|
4324
4390
|
// src/runtime/services/file.service.ts
|
|
4325
4391
|
var FileService = class {
|
|
4326
|
-
constructor(adapter, tenantId) {
|
|
4392
|
+
constructor(adapter, tenantId, options) {
|
|
4327
4393
|
this.adapter = adapter;
|
|
4328
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;
|
|
4329
4398
|
}
|
|
4399
|
+
// ============================================================================
|
|
4400
|
+
// UPLOAD (requires StorageAdapter)
|
|
4401
|
+
// ============================================================================
|
|
4330
4402
|
/**
|
|
4331
|
-
*
|
|
4403
|
+
* Upload a file to storage and create metadata record.
|
|
4332
4404
|
*
|
|
4333
|
-
*
|
|
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
|
|
4334
4413
|
* @returns Created file record
|
|
4414
|
+
* @throws Error if StorageAdapter is not configured
|
|
4335
4415
|
*
|
|
4336
4416
|
* @example
|
|
4337
4417
|
* ```typescript
|
|
4338
|
-
* 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).
|
|
4481
|
+
*
|
|
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
|
|
4339
4487
|
*
|
|
4340
|
-
*
|
|
4488
|
+
* @example
|
|
4489
|
+
* ```typescript
|
|
4490
|
+
* // After uploading to S3 with Multer
|
|
4341
4491
|
* const file = await service.createFile({
|
|
4342
4492
|
* tenantId: "tenant-123",
|
|
4343
4493
|
* name: "contract-2025.pdf",
|
|
@@ -4359,8 +4509,26 @@ var FileService = class {
|
|
|
4359
4509
|
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
4360
4510
|
);
|
|
4361
4511
|
}
|
|
4362
|
-
|
|
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;
|
|
4363
4528
|
}
|
|
4529
|
+
// ============================================================================
|
|
4530
|
+
// READ
|
|
4531
|
+
// ============================================================================
|
|
4364
4532
|
/**
|
|
4365
4533
|
* Get file by ID
|
|
4366
4534
|
*/
|
|
@@ -4381,6 +4549,9 @@ var FileService = class {
|
|
|
4381
4549
|
}
|
|
4382
4550
|
return file2;
|
|
4383
4551
|
}
|
|
4552
|
+
// ============================================================================
|
|
4553
|
+
// UPDATE
|
|
4554
|
+
// ============================================================================
|
|
4384
4555
|
/**
|
|
4385
4556
|
* Update file metadata
|
|
4386
4557
|
*
|
|
@@ -4389,18 +4560,44 @@ var FileService = class {
|
|
|
4389
4560
|
* @returns Updated file
|
|
4390
4561
|
*/
|
|
4391
4562
|
async updateFile(fileId, data) {
|
|
4392
|
-
await this.getFileOrThrow(fileId);
|
|
4393
|
-
|
|
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;
|
|
4394
4588
|
}
|
|
4589
|
+
// ============================================================================
|
|
4590
|
+
// DELETE
|
|
4591
|
+
// ============================================================================
|
|
4395
4592
|
/**
|
|
4396
|
-
* Delete file (soft delete)
|
|
4593
|
+
* Delete file (soft delete by default)
|
|
4397
4594
|
*
|
|
4398
4595
|
* @param fileId - File UUID
|
|
4399
4596
|
* @param options - Delete options
|
|
4400
4597
|
*/
|
|
4401
4598
|
async deleteFile(fileId, options) {
|
|
4599
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
4402
4600
|
if (options?.checkOwnership && options.userId) {
|
|
4403
|
-
const file2 = await this.getFileOrThrow(fileId);
|
|
4404
4601
|
if (file2.uploadedBy !== options.userId) {
|
|
4405
4602
|
throw new Error("You can only delete files you uploaded");
|
|
4406
4603
|
}
|
|
@@ -4410,7 +4607,84 @@ var FileService = class {
|
|
|
4410
4607
|
} else {
|
|
4411
4608
|
await this.adapter.files.delete(fileId);
|
|
4412
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
|
+
}
|
|
4413
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
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
// ============================================================================
|
|
4686
|
+
// LIST
|
|
4687
|
+
// ============================================================================
|
|
4414
4688
|
/**
|
|
4415
4689
|
* List files for the tenant
|
|
4416
4690
|
*/
|
|
@@ -4429,6 +4703,74 @@ var FileService = class {
|
|
|
4429
4703
|
async listFilesByUploader(uploadedBy) {
|
|
4430
4704
|
return await this.adapter.files.findByUploader(uploadedBy);
|
|
4431
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
|
+
}
|
|
4432
4774
|
/**
|
|
4433
4775
|
* Change file visibility
|
|
4434
4776
|
*
|
|
@@ -4472,29 +4814,9 @@ var FileService = class {
|
|
|
4472
4814
|
allowedUsers: newAllowed
|
|
4473
4815
|
});
|
|
4474
4816
|
}
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
* @param fileId - File UUID
|
|
4479
|
-
* @param userId - User ID to check
|
|
4480
|
-
* @returns true if user can access the file
|
|
4481
|
-
*/
|
|
4482
|
-
async canAccess(fileId, userId) {
|
|
4483
|
-
const file2 = await this.getFile(fileId);
|
|
4484
|
-
if (!file2) {
|
|
4485
|
-
return false;
|
|
4486
|
-
}
|
|
4487
|
-
if (file2.visibility === "public") {
|
|
4488
|
-
return true;
|
|
4489
|
-
}
|
|
4490
|
-
if (file2.uploadedBy === userId) {
|
|
4491
|
-
return true;
|
|
4492
|
-
}
|
|
4493
|
-
if (file2.visibility === "restricted") {
|
|
4494
|
-
return file2.allowedUsers?.includes(userId) ?? false;
|
|
4495
|
-
}
|
|
4496
|
-
return false;
|
|
4497
|
-
}
|
|
4817
|
+
// ============================================================================
|
|
4818
|
+
// ORGANIZATION
|
|
4819
|
+
// ============================================================================
|
|
4498
4820
|
/**
|
|
4499
4821
|
* Move file to different folder
|
|
4500
4822
|
*/
|
|
@@ -6330,6 +6652,123 @@ var RelationService = class {
|
|
|
6330
6652
|
}
|
|
6331
6653
|
};
|
|
6332
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
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
};
|
|
6771
|
+
|
|
6333
6772
|
// src/runtime/services/record.service.ts
|
|
6334
6773
|
var RecordService = class {
|
|
6335
6774
|
constructor(adapter, tenantId, options) {
|
|
@@ -6337,6 +6776,7 @@ var RecordService = class {
|
|
|
6337
6776
|
this.tenantId = tenantId;
|
|
6338
6777
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
6339
6778
|
this.relationService = new RelationService(adapter, registry);
|
|
6779
|
+
this.userService = new UserService(adapter, tenantId);
|
|
6340
6780
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
6341
6781
|
this.permissionService = options?.permissionService;
|
|
6342
6782
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -6477,6 +6917,9 @@ var RecordService = class {
|
|
|
6477
6917
|
if (!options?.skipRelationValidation) {
|
|
6478
6918
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6479
6919
|
}
|
|
6920
|
+
if (!options?.skipUserValidation) {
|
|
6921
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
6922
|
+
}
|
|
6480
6923
|
}
|
|
6481
6924
|
const completionStatus = computeRecordStatus(schema, data);
|
|
6482
6925
|
const label = await this.computeLabel(schema, data);
|
|
@@ -6588,6 +7031,9 @@ var RecordService = class {
|
|
|
6588
7031
|
if (!options?.skipRelationValidation) {
|
|
6589
7032
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6590
7033
|
}
|
|
7034
|
+
if (!options?.skipUserValidation) {
|
|
7035
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
7036
|
+
}
|
|
6591
7037
|
}
|
|
6592
7038
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
6593
7039
|
const label = await this.computeLabel(schema, mergedData);
|
|
@@ -7245,7 +7691,7 @@ var ViewService = class {
|
|
|
7245
7691
|
throw new NotFoundError("View", viewId);
|
|
7246
7692
|
}
|
|
7247
7693
|
if (dbView.system) {
|
|
7248
|
-
throw new ProtectedResourceError("
|
|
7694
|
+
throw new ProtectedResourceError("view", dbView.name, "modify");
|
|
7249
7695
|
}
|
|
7250
7696
|
const updated = await this.adapter.views.update(viewId, {
|
|
7251
7697
|
label: input.label,
|
|
@@ -7268,7 +7714,7 @@ var ViewService = class {
|
|
|
7268
7714
|
throw new NotFoundError("View", viewId);
|
|
7269
7715
|
}
|
|
7270
7716
|
if (dbView.system) {
|
|
7271
|
-
throw new ProtectedResourceError("
|
|
7717
|
+
throw new ProtectedResourceError("view", dbView.name, "delete");
|
|
7272
7718
|
}
|
|
7273
7719
|
await this.adapter.views.delete(viewId);
|
|
7274
7720
|
}
|
|
@@ -7717,6 +8163,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7717
8163
|
TableTabConfig,
|
|
7718
8164
|
UserProfileNotFoundError,
|
|
7719
8165
|
UserProfileService,
|
|
8166
|
+
UserService,
|
|
7720
8167
|
ValidationError,
|
|
7721
8168
|
ViewBuilder,
|
|
7722
8169
|
ViewService,
|