@yunsoft/yuncms-core 0.1.0 → 0.1.2
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/README.md +6 -0
- package/package.json +1 -1
- package/src/auth/users-repository.js +2 -2
- package/src/bootstrap.js +14 -0
- package/src/config.js +4 -1
- package/src/field-types.js +49 -1
- package/src/index.js +24 -3
- package/src/migrations/0005-default-public-role.js +16 -0
- package/src/migrations/0006-studio-settings.js +20 -0
- package/src/migrations/0007-system-permission-resources.js +46 -0
- package/src/o2o-relation.js +231 -0
- package/src/schema-metadata-repository.js +17 -0
- package/src/services/auth-service.js +4 -1
- package/src/services/collections-service.js +56 -8
- package/src/services/core-services.js +2 -0
- package/src/services/fields-service.js +63 -27
- package/src/services/files-service.js +24 -23
- package/src/services/items-service.js +34 -12
- package/src/services/permissions-service.js +48 -13
- package/src/services/roles-service.js +4 -3
- package/src/services/sessions-service.js +3 -3
- package/src/services/studio-settings-service.js +140 -0
- package/src/services/system-resource-access.js +23 -0
- package/src/services/users-service.js +71 -28
- package/src/setup.js +50 -2
- package/src/system-fields.js +146 -0
- package/src/system-permissions.js +54 -0
|
@@ -4,6 +4,7 @@ import { withAdvisoryLock } from '../advisory-lock.js';
|
|
|
4
4
|
import { assertIdentifier, quoteIdentifier } from '../identifier.js';
|
|
5
5
|
import { SchemaMetadataRepository } from '../schema-metadata-repository.js';
|
|
6
6
|
import { incrementSchemaVersion } from '../schema-version.js';
|
|
7
|
+
import { compileCollectionSystemFields, normalizeCollectionSystemFields } from '../system-fields.js';
|
|
7
8
|
import { withConnectionTransaction } from '../transaction.js';
|
|
8
9
|
import { BaseService } from './base-service.js';
|
|
9
10
|
import { assertSchemaManager } from './schema-access.js';
|
|
@@ -21,11 +22,15 @@ function assertUserCollectionName(collection) {
|
|
|
21
22
|
return collection;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function invalidSchemaPayload(message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
function assertCollectionMetadataPatch(patch) {
|
|
25
32
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
26
|
-
|
|
27
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
28
|
-
throw error;
|
|
33
|
+
throw invalidSchemaPayload('Collection metadata patch must be an object');
|
|
29
34
|
}
|
|
30
35
|
for (const key of Object.keys(patch)) {
|
|
31
36
|
if (!COLLECTION_METADATA_KEYS.has(key)) {
|
|
@@ -35,12 +40,40 @@ function assertCollectionMetadataPatch(patch) {
|
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
if (Object.keys(patch).length === 0) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
throw invalidSchemaPayload('Collection metadata patch cannot be empty');
|
|
44
|
+
}
|
|
45
|
+
for (const key of ['singleton', 'hidden']) {
|
|
46
|
+
if (Object.hasOwn(patch, key) && typeof patch[key] !== 'boolean') {
|
|
47
|
+
throw invalidSchemaPayload(`Collection ${key} must be a boolean`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (Object.hasOwn(patch, 'note') && patch.note != null && typeof patch.note !== 'string') {
|
|
51
|
+
throw invalidSchemaPayload('Collection note must be a string or null');
|
|
52
|
+
}
|
|
53
|
+
if (Object.hasOwn(patch, 'metadata') && patch.metadata != null && (
|
|
54
|
+
typeof patch.metadata !== 'object' || Array.isArray(patch.metadata)
|
|
55
|
+
)) {
|
|
56
|
+
throw invalidSchemaPayload('Collection metadata must be an object or null');
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
function assertCollectionCreateMetadata(input) {
|
|
61
|
+
for (const key of ['singleton', 'hidden']) {
|
|
62
|
+
if (Object.hasOwn(input, key) && typeof input[key] !== 'boolean') {
|
|
63
|
+
throw invalidSchemaPayload(`Collection ${key} must be a boolean`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (Object.hasOwn(input, 'note') && input.note != null && typeof input.note !== 'string') {
|
|
67
|
+
throw invalidSchemaPayload('Collection note must be a string or null');
|
|
68
|
+
}
|
|
69
|
+
if (Object.hasOwn(input, 'metadata') && input.metadata != null && (
|
|
70
|
+
typeof input.metadata !== 'object' || Array.isArray(input.metadata)
|
|
71
|
+
)) {
|
|
72
|
+
throw invalidSchemaPayload('Collection metadata must be an object or null');
|
|
73
|
+
}
|
|
74
|
+
normalizeCollectionSystemFields(input.systemFields);
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
function temporaryDropName() {
|
|
45
78
|
return `_yuncms_drop_${randomUUID().replaceAll('-', '').slice(0, 24)}`;
|
|
46
79
|
}
|
|
@@ -59,6 +92,7 @@ export class CollectionsService extends BaseService {
|
|
|
59
92
|
|
|
60
93
|
async createOne(input = {}) {
|
|
61
94
|
assertSchemaManager(this.accountability);
|
|
95
|
+
assertCollectionCreateMetadata(input);
|
|
62
96
|
const collection = assertUserCollectionName(input.collection);
|
|
63
97
|
const primaryKey = input.primaryKey ?? 'id';
|
|
64
98
|
|
|
@@ -68,6 +102,8 @@ export class CollectionsService extends BaseService {
|
|
|
68
102
|
throw error;
|
|
69
103
|
}
|
|
70
104
|
|
|
105
|
+
const systemFields = compileCollectionSystemFields(collection, input.systemFields);
|
|
106
|
+
|
|
71
107
|
return withAdvisoryLock(this.database, 'yuncms:schema', async (connection) => {
|
|
72
108
|
const metadata = new SchemaMetadataRepository(connection);
|
|
73
109
|
const existing = await metadata.readCollection(collection);
|
|
@@ -79,11 +115,16 @@ export class CollectionsService extends BaseService {
|
|
|
79
115
|
|
|
80
116
|
const table = quoteIdentifier(collection, 'collection name');
|
|
81
117
|
let tableCreated = false;
|
|
118
|
+
const physicalDefinitions = [
|
|
119
|
+
'id CHAR(36) NOT NULL PRIMARY KEY',
|
|
120
|
+
...systemFields.columns,
|
|
121
|
+
...systemFields.constraints,
|
|
122
|
+
];
|
|
82
123
|
|
|
83
124
|
try {
|
|
84
125
|
await connection.query(
|
|
85
126
|
`CREATE TABLE ${table} (
|
|
86
|
-
|
|
127
|
+
${physicalDefinitions.join(',\n ')}
|
|
87
128
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
88
129
|
);
|
|
89
130
|
tableCreated = true;
|
|
@@ -95,7 +136,10 @@ export class CollectionsService extends BaseService {
|
|
|
95
136
|
note: input.note ?? null,
|
|
96
137
|
singleton: input.singleton === true,
|
|
97
138
|
hidden: input.hidden === true,
|
|
98
|
-
metadata:
|
|
139
|
+
metadata: {
|
|
140
|
+
...(input.metadata ?? {}),
|
|
141
|
+
systemFields: systemFields.fields,
|
|
142
|
+
},
|
|
99
143
|
});
|
|
100
144
|
|
|
101
145
|
await metadata.createField({
|
|
@@ -108,6 +152,10 @@ export class CollectionsService extends BaseService {
|
|
|
108
152
|
schemaMetadata: { primaryKey: true, length: 36 },
|
|
109
153
|
});
|
|
110
154
|
|
|
155
|
+
for (const systemField of systemFields.metadata) {
|
|
156
|
+
await metadata.createField(systemField);
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
const schemaVersion = await incrementSchemaVersion(connection);
|
|
112
160
|
return { ...created, schemaVersion };
|
|
113
161
|
});
|
|
@@ -10,6 +10,7 @@ import { ItemsService } from './items-service.js';
|
|
|
10
10
|
import { PermissionsService } from './permissions-service.js';
|
|
11
11
|
import { RelationsService } from './relations-service.js';
|
|
12
12
|
import { RolesService } from './roles-service.js';
|
|
13
|
+
import { StudioSettingsService } from './studio-settings-service.js';
|
|
13
14
|
import { UsersService } from './users-service.js';
|
|
14
15
|
import { createServiceRegistry } from './service-registry.js';
|
|
15
16
|
|
|
@@ -28,5 +29,6 @@ export function createCoreServiceRegistry() {
|
|
|
28
29
|
PermissionsService,
|
|
29
30
|
FilesService,
|
|
30
31
|
FileReconciliationService,
|
|
32
|
+
StudioSettingsService,
|
|
31
33
|
});
|
|
32
34
|
}
|
|
@@ -10,7 +10,15 @@ import { BaseService } from './base-service.js';
|
|
|
10
10
|
import { assertSchemaManager } from './schema-access.js';
|
|
11
11
|
|
|
12
12
|
const FIELD_METADATA_KEYS = new Set(['readonly', 'hidden', 'sort', 'interface', 'options']);
|
|
13
|
-
const FIELD_PHYSICAL_KEYS = new Set([
|
|
13
|
+
const FIELD_PHYSICAL_KEYS = new Set([
|
|
14
|
+
'required',
|
|
15
|
+
'defaultValue',
|
|
16
|
+
'removeDefault',
|
|
17
|
+
'defaultPreset',
|
|
18
|
+
'removeDefaultPreset',
|
|
19
|
+
'autoUpdate',
|
|
20
|
+
'indexed',
|
|
21
|
+
]);
|
|
14
22
|
|
|
15
23
|
function assertFieldName(field) {
|
|
16
24
|
assertIdentifier(field, 'field name');
|
|
@@ -18,11 +26,15 @@ function assertFieldName(field) {
|
|
|
18
26
|
return field;
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
function invalidSchemaPayload(message) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
function assertFieldMetadataPatch(patch) {
|
|
22
36
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
23
|
-
|
|
24
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
25
|
-
throw error;
|
|
37
|
+
throw invalidSchemaPayload('Field metadata patch must be an object');
|
|
26
38
|
}
|
|
27
39
|
for (const key of Object.keys(patch)) {
|
|
28
40
|
if (!FIELD_METADATA_KEYS.has(key)) {
|
|
@@ -32,23 +44,17 @@ function assertFieldMetadataPatch(patch) {
|
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
if (Object.keys(patch).length === 0) {
|
|
35
|
-
|
|
36
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
37
|
-
throw error;
|
|
47
|
+
throw invalidSchemaPayload('Field metadata patch cannot be empty');
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
function assertPhysicalPatch(patch) {
|
|
42
52
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
43
|
-
|
|
44
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
45
|
-
throw error;
|
|
53
|
+
throw invalidSchemaPayload('Physical field patch must be an object');
|
|
46
54
|
}
|
|
47
55
|
const keys = Object.keys(patch);
|
|
48
56
|
if (keys.length === 0) {
|
|
49
|
-
|
|
50
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
51
|
-
throw error;
|
|
57
|
+
throw invalidSchemaPayload('Physical field patch cannot be empty');
|
|
52
58
|
}
|
|
53
59
|
for (const key of keys) {
|
|
54
60
|
if (!FIELD_PHYSICAL_KEYS.has(key)) {
|
|
@@ -58,24 +64,30 @@ function assertPhysicalPatch(patch) {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
if (Object.hasOwn(patch, 'required') && typeof patch.required !== 'boolean') {
|
|
61
|
-
|
|
62
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
63
|
-
throw error;
|
|
67
|
+
throw invalidSchemaPayload('required must be boolean');
|
|
64
68
|
}
|
|
65
69
|
if (Object.hasOwn(patch, 'indexed') && typeof patch.indexed !== 'boolean') {
|
|
66
|
-
|
|
67
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
68
|
-
throw error;
|
|
70
|
+
throw invalidSchemaPayload('indexed must be boolean');
|
|
69
71
|
}
|
|
70
|
-
if (Object.hasOwn(patch, '
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (Object.hasOwn(patch, 'autoUpdate') && typeof patch.autoUpdate !== 'boolean') {
|
|
73
|
+
throw invalidSchemaPayload('autoUpdate must be boolean');
|
|
74
|
+
}
|
|
75
|
+
for (const key of ['removeDefault', 'removeDefaultPreset']) {
|
|
76
|
+
if (Object.hasOwn(patch, key) && patch[key] !== true) {
|
|
77
|
+
throw invalidSchemaPayload(`${key} must be true when provided`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (Object.hasOwn(patch, 'defaultPreset') && patch.defaultPreset !== 'now') {
|
|
81
|
+
throw invalidSchemaPayload('defaultPreset currently supports only now');
|
|
82
|
+
}
|
|
83
|
+
if (Object.hasOwn(patch, 'defaultValue') && Object.hasOwn(patch, 'defaultPreset')) {
|
|
84
|
+
throw invalidSchemaPayload('defaultValue and defaultPreset cannot be used together');
|
|
74
85
|
}
|
|
75
86
|
if (Object.hasOwn(patch, 'defaultValue') && patch.removeDefault === true) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
throw invalidSchemaPayload('defaultValue and removeDefault cannot be used together');
|
|
88
|
+
}
|
|
89
|
+
if (Object.hasOwn(patch, 'defaultPreset') && patch.removeDefaultPreset === true) {
|
|
90
|
+
throw invalidSchemaPayload('defaultPreset and removeDefaultPreset cannot be used together');
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -89,15 +101,26 @@ function parseSchemaMetadata(value) {
|
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
function assertFieldNotSystemManaged(field) {
|
|
105
|
+
if (parseSchemaMetadata(field?.schema_metadata).systemManaged === true) {
|
|
106
|
+
const error = new Error(`System-managed field cannot be changed directly: ${field.collection}.${field.field}`);
|
|
107
|
+
error.code = 'SYSTEM_SCHEMA_READ_ONLY';
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function fieldInputFromMetadata(field, schemaMetadata) {
|
|
93
113
|
const input = {
|
|
94
114
|
type: field.type,
|
|
95
115
|
required: Boolean(field.required),
|
|
116
|
+
interface: field.interface ?? null,
|
|
96
117
|
};
|
|
97
118
|
if (schemaMetadata.length !== undefined && field.type === 'string') input.length = schemaMetadata.length;
|
|
98
119
|
if (schemaMetadata.precision !== undefined && field.type === 'decimal') input.precision = schemaMetadata.precision;
|
|
99
120
|
if (schemaMetadata.scale !== undefined && field.type === 'decimal') input.scale = schemaMetadata.scale;
|
|
100
121
|
if (Object.hasOwn(schemaMetadata, 'defaultValue')) input.defaultValue = schemaMetadata.defaultValue;
|
|
122
|
+
if (schemaMetadata.defaultPreset !== undefined) input.defaultPreset = schemaMetadata.defaultPreset;
|
|
123
|
+
if (schemaMetadata.autoUpdate === true) input.autoUpdate = true;
|
|
101
124
|
return input;
|
|
102
125
|
}
|
|
103
126
|
|
|
@@ -246,6 +269,7 @@ export class FieldsService extends BaseService {
|
|
|
246
269
|
error.code = 'FIELD_NOT_FOUND';
|
|
247
270
|
throw error;
|
|
248
271
|
}
|
|
272
|
+
assertFieldNotSystemManaged(existing);
|
|
249
273
|
|
|
250
274
|
return withConnectionTransaction(connection, async () => {
|
|
251
275
|
const updated = await metadata.updateFieldMetadata(collection, field, patch);
|
|
@@ -288,6 +312,7 @@ export class FieldsService extends BaseService {
|
|
|
288
312
|
error.code = 'FIELD_NOT_FOUND';
|
|
289
313
|
throw error;
|
|
290
314
|
}
|
|
315
|
+
assertFieldNotSystemManaged(existing);
|
|
291
316
|
|
|
292
317
|
const relation = relations.find((candidate) =>
|
|
293
318
|
candidate.many_collection === collection && candidate.many_field === field);
|
|
@@ -301,8 +326,18 @@ export class FieldsService extends BaseService {
|
|
|
301
326
|
const currentMetadata = parseSchemaMetadata(existing.schema_metadata);
|
|
302
327
|
const currentInput = fieldInputFromMetadata(existing, currentMetadata);
|
|
303
328
|
const nextInput = { ...currentInput, required: nextRequired };
|
|
329
|
+
|
|
304
330
|
if (patch.removeDefault === true) delete nextInput.defaultValue;
|
|
305
|
-
|
|
331
|
+
if (patch.removeDefaultPreset === true) delete nextInput.defaultPreset;
|
|
332
|
+
if (Object.hasOwn(patch, 'defaultValue')) {
|
|
333
|
+
nextInput.defaultValue = patch.defaultValue;
|
|
334
|
+
delete nextInput.defaultPreset;
|
|
335
|
+
}
|
|
336
|
+
if (Object.hasOwn(patch, 'defaultPreset')) {
|
|
337
|
+
nextInput.defaultPreset = patch.defaultPreset;
|
|
338
|
+
delete nextInput.defaultValue;
|
|
339
|
+
}
|
|
340
|
+
if (Object.hasOwn(patch, 'autoUpdate')) nextInput.autoUpdate = patch.autoUpdate;
|
|
306
341
|
|
|
307
342
|
const currentCompiled = compileFieldColumn(currentInput);
|
|
308
343
|
const nextCompiled = compileFieldColumn(nextInput);
|
|
@@ -410,6 +445,7 @@ export class FieldsService extends BaseService {
|
|
|
410
445
|
error.code = 'FIELD_NOT_FOUND';
|
|
411
446
|
throw error;
|
|
412
447
|
}
|
|
448
|
+
assertFieldNotSystemManaged(existing);
|
|
413
449
|
|
|
414
450
|
const blockingRelation = relations.find((relation) =>
|
|
415
451
|
(relation.many_collection === collection && relation.many_field === field) ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { BaseService } from './base-service.js';
|
|
4
|
+
import { resolveSystemResourceAccess } from './system-resource-access.js';
|
|
4
5
|
|
|
5
6
|
function fileError(code, message) {
|
|
6
7
|
const error = new Error(message);
|
|
@@ -8,11 +9,6 @@ function fileError(code, message) {
|
|
|
8
9
|
return error;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
function assertFileManager(accountability) {
|
|
12
|
-
if (accountability.admin === true || accountability.system === true) return;
|
|
13
|
-
throw fileError('FORBIDDEN', 'File management requires administrator accountability in V1');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
function normalizeFilename(value) {
|
|
17
13
|
if (typeof value !== 'string') throw fileError('INVALID_PAYLOAD', 'Download filename is required');
|
|
18
14
|
const filename = value.trim();
|
|
@@ -60,28 +56,32 @@ export class FilesService extends BaseService {
|
|
|
60
56
|
});
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
async
|
|
64
|
-
assertFileManager(this.accountability);
|
|
59
|
+
async #readOneUnsafe(id) {
|
|
65
60
|
const [rows] = await this.database.query(
|
|
66
61
|
`SELECT id, storage, filename_disk, filename_download, title, mimetype, filesize,
|
|
67
62
|
uploaded_by, uploaded_at, metadata
|
|
68
63
|
FROM yuncms_files
|
|
69
|
-
|
|
64
|
+
WHERE id = ?
|
|
65
|
+
LIMIT 1`,
|
|
66
|
+
[id],
|
|
70
67
|
);
|
|
71
|
-
return rows
|
|
68
|
+
return normalizeRow(rows[0]);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
async
|
|
75
|
-
|
|
71
|
+
async readMany() {
|
|
72
|
+
await resolveSystemResourceAccess(this, 'read', 'yuncms_files');
|
|
76
73
|
const [rows] = await this.database.query(
|
|
77
74
|
`SELECT id, storage, filename_disk, filename_download, title, mimetype, filesize,
|
|
78
75
|
uploaded_by, uploaded_at, metadata
|
|
79
76
|
FROM yuncms_files
|
|
80
|
-
|
|
81
|
-
LIMIT 1`,
|
|
82
|
-
[id],
|
|
77
|
+
ORDER BY uploaded_at DESC, id DESC`,
|
|
83
78
|
);
|
|
84
|
-
return normalizeRow
|
|
79
|
+
return rows.map(normalizeRow);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async readOne(id) {
|
|
83
|
+
await resolveSystemResourceAccess(this, 'read', 'yuncms_files');
|
|
84
|
+
return this.#readOneUnsafe(id);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
async createOne({
|
|
@@ -92,7 +92,7 @@ export class FilesService extends BaseService {
|
|
|
92
92
|
storage = 'local',
|
|
93
93
|
metadata = null,
|
|
94
94
|
} = {}) {
|
|
95
|
-
|
|
95
|
+
await resolveSystemResourceAccess(this, 'create', 'yuncms_files');
|
|
96
96
|
if (!Buffer.isBuffer(contents) && !(contents instanceof Uint8Array)) {
|
|
97
97
|
throw fileError('INVALID_FILE_CONTENT', 'File contents must be Buffer or Uint8Array');
|
|
98
98
|
}
|
|
@@ -124,7 +124,7 @@ export class FilesService extends BaseService {
|
|
|
124
124
|
metadata == null ? null : JSON.stringify(metadata),
|
|
125
125
|
],
|
|
126
126
|
);
|
|
127
|
-
const file = await this
|
|
127
|
+
const file = await this.#readOneUnsafe(id);
|
|
128
128
|
await this.action('files.create', {
|
|
129
129
|
key: id,
|
|
130
130
|
item: file,
|
|
@@ -144,7 +144,8 @@ export class FilesService extends BaseService {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
async readContent(id) {
|
|
147
|
-
|
|
147
|
+
await resolveSystemResourceAccess(this, 'read', 'yuncms_files');
|
|
148
|
+
const file = await this.#readOneUnsafe(id);
|
|
148
149
|
if (!file) throw fileError('FILE_NOT_FOUND', `Unknown file: ${id}`);
|
|
149
150
|
const driver = this.storage.get(file.storage);
|
|
150
151
|
const contents = await driver.get(file.filename_disk);
|
|
@@ -152,7 +153,7 @@ export class FilesService extends BaseService {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
async updateOne(id, patch = {}) {
|
|
155
|
-
|
|
156
|
+
await resolveSystemResourceAccess(this, 'update', 'yuncms_files');
|
|
156
157
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
157
158
|
throw fileError('INVALID_PAYLOAD', 'File metadata patch must be an object');
|
|
158
159
|
}
|
|
@@ -161,7 +162,7 @@ export class FilesService extends BaseService {
|
|
|
161
162
|
throw fileError('INVALID_PAYLOAD', 'File update supports filenameDownload, title and metadata only');
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const before = await this
|
|
165
|
+
const before = await this.#readOneUnsafe(id);
|
|
165
166
|
if (!before) throw fileError('FILE_NOT_FOUND', `Unknown file: ${id}`);
|
|
166
167
|
|
|
167
168
|
const assignments = [];
|
|
@@ -185,7 +186,7 @@ export class FilesService extends BaseService {
|
|
|
185
186
|
params,
|
|
186
187
|
);
|
|
187
188
|
if (result.affectedRows !== 1) throw fileError('FILE_NOT_FOUND', `Unknown file: ${id}`);
|
|
188
|
-
const file = await this
|
|
189
|
+
const file = await this.#readOneUnsafe(id);
|
|
189
190
|
await this.action('files.update', {
|
|
190
191
|
key: id,
|
|
191
192
|
before,
|
|
@@ -196,8 +197,8 @@ export class FilesService extends BaseService {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
async deleteOne(id) {
|
|
199
|
-
|
|
200
|
-
const file = await this
|
|
200
|
+
await resolveSystemResourceAccess(this, 'delete', 'yuncms_files');
|
|
201
|
+
const file = await this.#readOneUnsafe(id);
|
|
201
202
|
if (!file) throw fileError('FILE_NOT_FOUND', `Unknown file: ${id}`);
|
|
202
203
|
|
|
203
204
|
const [result] = await this.database.query('DELETE FROM yuncms_files WHERE id = ?', [id]);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
parseItemsQuery,
|
|
10
10
|
} from '../query.js';
|
|
11
11
|
import { SchemaCache } from '../schema.js';
|
|
12
|
+
import { isSystemManagedField, systemMutationEntries } from '../system-fields.js';
|
|
12
13
|
import { withTransaction } from '../transaction.js';
|
|
13
14
|
import { BaseService } from './base-service.js';
|
|
14
15
|
import { PermissionsService } from './permissions-service.js';
|
|
@@ -74,6 +75,10 @@ function createCandidateRecord(schema, id, entries) {
|
|
|
74
75
|
continue;
|
|
75
76
|
}
|
|
76
77
|
const metadata = parseMetadata(fieldSchema.schema_metadata);
|
|
78
|
+
if (metadata.defaultPreset === 'now') {
|
|
79
|
+
candidate[fieldSchema.field] = new Date();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
77
82
|
candidate[fieldSchema.field] = Object.hasOwn(metadata, 'defaultValue')
|
|
78
83
|
? metadata.defaultValue
|
|
79
84
|
: null;
|
|
@@ -82,6 +87,10 @@ function createCandidateRecord(schema, id, entries) {
|
|
|
82
87
|
return candidate;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
function mergeSystemEntries(entries, schema, accountability, operation, now = new Date()) {
|
|
91
|
+
return [...entries, ...systemMutationEntries(schema, accountability, operation, now)];
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
export class ItemsService extends BaseService {
|
|
86
95
|
constructor(collection, options = {}) {
|
|
87
96
|
super(options);
|
|
@@ -114,6 +123,12 @@ export class ItemsService extends BaseService {
|
|
|
114
123
|
if (!collectionSchema) {
|
|
115
124
|
throw serviceError('COLLECTION_NOT_FOUND', `Unknown collection: ${this.collection}`);
|
|
116
125
|
}
|
|
126
|
+
if (collectionSchema.system) {
|
|
127
|
+
throw serviceError(
|
|
128
|
+
'SYSTEM_COLLECTION_USE_DEDICATED_SERVICE',
|
|
129
|
+
`System collection ${this.collection} must be accessed through its dedicated service`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
117
132
|
return collectionSchema;
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -150,8 +165,9 @@ export class ItemsService extends BaseService {
|
|
|
150
165
|
for (const fieldSchema of Object.values(schema.fields)) {
|
|
151
166
|
if (!fieldSchema.required || fieldSchema.field === schema.primary_key) continue;
|
|
152
167
|
if (Object.hasOwn(payload, fieldSchema.field)) continue;
|
|
168
|
+
if (isSystemManagedField(fieldSchema)) continue;
|
|
153
169
|
const metadata = parseMetadata(fieldSchema.schema_metadata);
|
|
154
|
-
if (Object.hasOwn(metadata, 'defaultValue')) continue;
|
|
170
|
+
if (Object.hasOwn(metadata, 'defaultValue') || metadata.defaultPreset != null) continue;
|
|
155
171
|
throw serviceError('REQUIRED_FIELD_MISSING', `Required field is missing: ${fieldSchema.field}`, fieldSchema.field);
|
|
156
172
|
}
|
|
157
173
|
}
|
|
@@ -230,7 +246,8 @@ export class ItemsService extends BaseService {
|
|
|
230
246
|
const schema = await this.getCollectionSchema();
|
|
231
247
|
const permission = await this.resolvePermission('create');
|
|
232
248
|
const filteredPayload = await this.filterMutation('items.create', payload, { operation: 'create' });
|
|
233
|
-
const
|
|
249
|
+
const callerEntries = this.validatePayload(filteredPayload, schema, permission, { creating: true });
|
|
250
|
+
const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create');
|
|
234
251
|
const id = randomUUID();
|
|
235
252
|
const candidate = createCandidateRecord(schema, id, entries);
|
|
236
253
|
enforcePermissionValidation(candidate, permission.validation, schema);
|
|
@@ -264,7 +281,8 @@ export class ItemsService extends BaseService {
|
|
|
264
281
|
operation: 'create',
|
|
265
282
|
bulk: true,
|
|
266
283
|
});
|
|
267
|
-
const
|
|
284
|
+
const callerEntries = this.validatePayload(filteredPayload, schema, permission, { creating: true });
|
|
285
|
+
const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'create');
|
|
268
286
|
const id = randomUUID();
|
|
269
287
|
const candidate = createCandidateRecord(schema, id, entries);
|
|
270
288
|
enforcePermissionValidation(candidate, permission.validation, schema);
|
|
@@ -306,8 +324,10 @@ export class ItemsService extends BaseService {
|
|
|
306
324
|
operation: 'update',
|
|
307
325
|
key: id,
|
|
308
326
|
});
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
327
|
+
const callerEntries = this.validatePayload(filteredPayload, schema, permission);
|
|
328
|
+
if (callerEntries.length === 0) throw serviceError('INVALID_PAYLOAD', 'Update payload cannot be empty');
|
|
329
|
+
const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update');
|
|
330
|
+
const effectiveChanges = Object.fromEntries(entries);
|
|
311
331
|
|
|
312
332
|
const table = quoteIdentifier(this.collection, 'collection name');
|
|
313
333
|
const filter = combineCompiledFilters(
|
|
@@ -322,7 +342,7 @@ export class ItemsService extends BaseService {
|
|
|
322
342
|
);
|
|
323
343
|
const current = currentRows[0];
|
|
324
344
|
if (!current) return null;
|
|
325
|
-
enforcePermissionValidation({ ...current, ...
|
|
345
|
+
enforcePermissionValidation({ ...current, ...effectiveChanges }, permission.validation, schema);
|
|
326
346
|
}
|
|
327
347
|
|
|
328
348
|
const setSql = entries.map(([field]) => `${quoteIdentifier(field, 'field name')} = ?`).join(', ');
|
|
@@ -336,7 +356,7 @@ export class ItemsService extends BaseService {
|
|
|
336
356
|
await this.actionMutation('items.update', {
|
|
337
357
|
key: id,
|
|
338
358
|
item: record,
|
|
339
|
-
changes:
|
|
359
|
+
changes: effectiveChanges,
|
|
340
360
|
}, { operation: 'update' });
|
|
341
361
|
return record;
|
|
342
362
|
}
|
|
@@ -354,8 +374,10 @@ export class ItemsService extends BaseService {
|
|
|
354
374
|
bulk: true,
|
|
355
375
|
filter: filterInput,
|
|
356
376
|
});
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
377
|
+
const callerEntries = this.validatePayload(filteredPayload, schema, permission);
|
|
378
|
+
if (callerEntries.length === 0) throw serviceError('INVALID_PAYLOAD', 'Update payload cannot be empty');
|
|
379
|
+
const entries = mergeSystemEntries(callerEntries, schema, this.accountability, 'update');
|
|
380
|
+
const effectiveChanges = Object.fromEntries(entries);
|
|
359
381
|
const filter = this.compileActionFilters(filterInput, permission.filter, accessSchema, schema);
|
|
360
382
|
const table = quoteIdentifier(this.collection, 'collection name');
|
|
361
383
|
|
|
@@ -371,7 +393,7 @@ export class ItemsService extends BaseService {
|
|
|
371
393
|
);
|
|
372
394
|
}
|
|
373
395
|
for (const row of rows) {
|
|
374
|
-
enforcePermissionValidation({ ...row, ...
|
|
396
|
+
enforcePermissionValidation({ ...row, ...effectiveChanges }, permission.validation, schema);
|
|
375
397
|
}
|
|
376
398
|
}
|
|
377
399
|
|
|
@@ -382,7 +404,7 @@ export class ItemsService extends BaseService {
|
|
|
382
404
|
);
|
|
383
405
|
await this.actionMutation('items.update', {
|
|
384
406
|
filter: filterInput,
|
|
385
|
-
changes:
|
|
407
|
+
changes: effectiveChanges,
|
|
386
408
|
affected: result.affectedRows,
|
|
387
409
|
}, { operation: 'update', bulk: true });
|
|
388
410
|
return result.affectedRows;
|
|
@@ -442,4 +464,4 @@ export class ItemsService extends BaseService {
|
|
|
442
464
|
}
|
|
443
465
|
}
|
|
444
466
|
|
|
445
|
-
export { MAX_BULK_VALIDATION_ROWS, createCandidateRecord };
|
|
467
|
+
export { MAX_BULK_VALIDATION_ROWS, createCandidateRecord };
|