@yunsoft/yuncms-core 0.1.1 → 0.1.3
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 +8 -0
- package/src/config.js +3 -1
- package/src/field-types.js +49 -1
- package/src/index.js +18 -1
- package/src/migrations/0007-system-permission-resources.js +46 -0
- package/src/migrations/0008-studio-logo-file.js +10 -0
- package/src/migrations/0009-schema-display-names.js +19 -0
- package/src/migrations/0010-studio-favicon-file.js +12 -0
- package/src/o2o-relation.js +231 -0
- package/src/schema-key.js +54 -0
- package/src/schema-metadata-repository.js +37 -8
- package/src/services/auth-service.js +4 -1
- package/src/services/collections-service.js +32 -4
- package/src/services/core-services.js +2 -0
- package/src/services/fields-service.js +74 -29
- 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 +83 -11
- package/src/services/system-collection-fields-service.js +122 -0
- package/src/services/system-resource-access.js +23 -0
- package/src/services/users-service.js +71 -28
- package/src/setup.js +1 -2
- package/src/system-fields.js +146 -0
- package/src/system-permissions.js +54 -0
|
@@ -2,6 +2,8 @@ function encodeJson(value) {
|
|
|
2
2
|
return value == null ? null : JSON.stringify(value);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
const FILE_INTERFACES = new Set(['file', 'image']);
|
|
6
|
+
|
|
5
7
|
export class SchemaMetadataRepository {
|
|
6
8
|
constructor(database) {
|
|
7
9
|
if (!database) throw new Error('Database handle is required');
|
|
@@ -10,7 +12,7 @@ export class SchemaMetadataRepository {
|
|
|
10
12
|
|
|
11
13
|
async listCollections() {
|
|
12
14
|
const [rows] = await this.database.query(
|
|
13
|
-
`SELECT collection, primary_key, note, singleton, hidden, \`system\`, metadata, created_at, updated_at
|
|
15
|
+
`SELECT collection, name, primary_key, note, singleton, hidden, \`system\`, metadata, created_at, updated_at
|
|
14
16
|
FROM yuncms_collections
|
|
15
17
|
ORDER BY collection ASC`,
|
|
16
18
|
);
|
|
@@ -19,7 +21,7 @@ export class SchemaMetadataRepository {
|
|
|
19
21
|
|
|
20
22
|
async readCollection(collection) {
|
|
21
23
|
const [rows] = await this.database.query(
|
|
22
|
-
`SELECT collection, primary_key, note, singleton, hidden, \`system\`, metadata, created_at, updated_at
|
|
24
|
+
`SELECT collection, name, primary_key, note, singleton, hidden, \`system\`, metadata, created_at, updated_at
|
|
23
25
|
FROM yuncms_collections
|
|
24
26
|
WHERE collection = ?
|
|
25
27
|
LIMIT 1`,
|
|
@@ -30,6 +32,7 @@ export class SchemaMetadataRepository {
|
|
|
30
32
|
|
|
31
33
|
async createCollection({
|
|
32
34
|
collection,
|
|
35
|
+
name = collection,
|
|
33
36
|
primaryKey = 'id',
|
|
34
37
|
note = null,
|
|
35
38
|
singleton = false,
|
|
@@ -39,10 +42,11 @@ export class SchemaMetadataRepository {
|
|
|
39
42
|
}) {
|
|
40
43
|
await this.database.query(
|
|
41
44
|
`INSERT INTO yuncms_collections
|
|
42
|
-
(collection, primary_key, note, singleton, hidden, \`system\`, metadata)
|
|
43
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
45
|
+
(collection, name, primary_key, note, singleton, hidden, \`system\`, metadata)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
44
47
|
[
|
|
45
48
|
collection,
|
|
49
|
+
name,
|
|
46
50
|
primaryKey,
|
|
47
51
|
note,
|
|
48
52
|
singleton ? 1 : 0,
|
|
@@ -58,6 +62,10 @@ export class SchemaMetadataRepository {
|
|
|
58
62
|
const assignments = [];
|
|
59
63
|
const params = [];
|
|
60
64
|
|
|
65
|
+
if (Object.hasOwn(patch, 'name')) {
|
|
66
|
+
assignments.push('name = ?');
|
|
67
|
+
params.push(patch.name);
|
|
68
|
+
}
|
|
61
69
|
if (Object.hasOwn(patch, 'note')) {
|
|
62
70
|
assignments.push('note = ?');
|
|
63
71
|
params.push(patch.note ?? null);
|
|
@@ -94,7 +102,7 @@ export class SchemaMetadataRepository {
|
|
|
94
102
|
|
|
95
103
|
async listFields(collection) {
|
|
96
104
|
const [rows] = await this.database.query(
|
|
97
|
-
`SELECT id, collection, field, type, required, readonly, hidden, sort, interface, options, schema_metadata,
|
|
105
|
+
`SELECT id, collection, field, name, type, required, readonly, hidden, sort, interface, options, schema_metadata,
|
|
98
106
|
created_at, updated_at
|
|
99
107
|
FROM yuncms_fields
|
|
100
108
|
WHERE collection = ?
|
|
@@ -106,7 +114,7 @@ export class SchemaMetadataRepository {
|
|
|
106
114
|
|
|
107
115
|
async readField(collection, field) {
|
|
108
116
|
const [rows] = await this.database.query(
|
|
109
|
-
`SELECT id, collection, field, type, required, readonly, hidden, sort, interface, options, schema_metadata,
|
|
117
|
+
`SELECT id, collection, field, name, type, required, readonly, hidden, sort, interface, options, schema_metadata,
|
|
110
118
|
created_at, updated_at
|
|
111
119
|
FROM yuncms_fields
|
|
112
120
|
WHERE collection = ? AND field = ?
|
|
@@ -119,6 +127,7 @@ export class SchemaMetadataRepository {
|
|
|
119
127
|
async createField({
|
|
120
128
|
collection,
|
|
121
129
|
field,
|
|
130
|
+
name = field,
|
|
122
131
|
type,
|
|
123
132
|
required = false,
|
|
124
133
|
readonly = false,
|
|
@@ -128,13 +137,19 @@ export class SchemaMetadataRepository {
|
|
|
128
137
|
options = null,
|
|
129
138
|
schemaMetadata = null,
|
|
130
139
|
}) {
|
|
140
|
+
if (FILE_INTERFACES.has(fieldInterface) && type !== 'uuid') {
|
|
141
|
+
const error = new Error(`${fieldInterface} interface requires uuid storage`);
|
|
142
|
+
error.code = 'INVALID_FIELD_INTERFACE';
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
131
145
|
await this.database.query(
|
|
132
146
|
`INSERT INTO yuncms_fields
|
|
133
|
-
(collection, field, type, required, readonly, hidden, sort, interface, options, schema_metadata)
|
|
134
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
147
|
+
(collection, field, name, type, required, readonly, hidden, sort, interface, options, schema_metadata)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
135
149
|
[
|
|
136
150
|
collection,
|
|
137
151
|
field,
|
|
152
|
+
name,
|
|
138
153
|
type,
|
|
139
154
|
required ? 1 : 0,
|
|
140
155
|
readonly ? 1 : 0,
|
|
@@ -152,6 +167,20 @@ export class SchemaMetadataRepository {
|
|
|
152
167
|
const assignments = [];
|
|
153
168
|
const params = [];
|
|
154
169
|
|
|
170
|
+
if (Object.hasOwn(patch, 'interface') && FILE_INTERFACES.has(patch.interface)) {
|
|
171
|
+
const existing = await this.readField(collection, field);
|
|
172
|
+
if (!existing) return null;
|
|
173
|
+
if (existing.type !== 'uuid') {
|
|
174
|
+
const error = new Error(`${patch.interface} interface requires uuid storage`);
|
|
175
|
+
error.code = 'INVALID_FIELD_INTERFACE';
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (Object.hasOwn(patch, 'name')) {
|
|
181
|
+
assignments.push('name = ?');
|
|
182
|
+
params.push(patch.name);
|
|
183
|
+
}
|
|
155
184
|
if (Object.hasOwn(patch, 'readonly')) {
|
|
156
185
|
assignments.push('readonly = ?');
|
|
157
186
|
params.push(patch.readonly ? 1 : 0);
|
|
@@ -31,6 +31,7 @@ function publicUser(user) {
|
|
|
31
31
|
id: user.id,
|
|
32
32
|
email: user.email,
|
|
33
33
|
role: user.role ?? null,
|
|
34
|
+
role_name: user.role_name ?? null,
|
|
34
35
|
status: user.status,
|
|
35
36
|
email_verified_at: user.email_verified_at ?? null,
|
|
36
37
|
};
|
|
@@ -95,7 +96,7 @@ export class AuthService extends BaseService {
|
|
|
95
96
|
if (tokenType(token) !== 'api') throw invalidToken();
|
|
96
97
|
const [rows] = await this.database.query(
|
|
97
98
|
`SELECT t.id AS api_token_id, t.user, u.email, u.role, u.status,
|
|
98
|
-
r.admin AS role_admin
|
|
99
|
+
r.name AS role_name, r.admin AS role_admin
|
|
99
100
|
FROM yuncms_api_tokens t
|
|
100
101
|
INNER JOIN yuncms_users u ON u.id = t.user
|
|
101
102
|
LEFT JOIN yuncms_roles r ON r.id = u.role
|
|
@@ -119,6 +120,7 @@ export class AuthService extends BaseService {
|
|
|
119
120
|
return {
|
|
120
121
|
user: row.user,
|
|
121
122
|
role: row.role ?? null,
|
|
123
|
+
role_name: row.role_name ?? null,
|
|
122
124
|
admin: Boolean(row.role_admin),
|
|
123
125
|
email: row.email,
|
|
124
126
|
session: null,
|
|
@@ -141,6 +143,7 @@ export class AuthService extends BaseService {
|
|
|
141
143
|
id: result.user,
|
|
142
144
|
email: result.email,
|
|
143
145
|
role: result.role,
|
|
146
|
+
role_name: result.role_name ?? null,
|
|
144
147
|
},
|
|
145
148
|
access_token: result.access_token,
|
|
146
149
|
access_expires_at: result.access_expires_at,
|
|
@@ -3,12 +3,14 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { withAdvisoryLock } from '../advisory-lock.js';
|
|
4
4
|
import { assertIdentifier, quoteIdentifier } from '../identifier.js';
|
|
5
5
|
import { SchemaMetadataRepository } from '../schema-metadata-repository.js';
|
|
6
|
+
import { normalizeDisplayName, resolveSchemaName } from '../schema-key.js';
|
|
6
7
|
import { incrementSchemaVersion } from '../schema-version.js';
|
|
8
|
+
import { compileCollectionSystemFields, normalizeCollectionSystemFields } from '../system-fields.js';
|
|
7
9
|
import { withConnectionTransaction } from '../transaction.js';
|
|
8
10
|
import { BaseService } from './base-service.js';
|
|
9
11
|
import { assertSchemaManager } from './schema-access.js';
|
|
10
12
|
|
|
11
|
-
const COLLECTION_METADATA_KEYS = new Set(['note', 'singleton', 'hidden', 'metadata']);
|
|
13
|
+
const COLLECTION_METADATA_KEYS = new Set(['name', 'note', 'singleton', 'hidden', 'metadata']);
|
|
12
14
|
|
|
13
15
|
function assertUserCollectionName(collection) {
|
|
14
16
|
assertIdentifier(collection, 'collection name');
|
|
@@ -46,6 +48,9 @@ function assertCollectionMetadataPatch(patch) {
|
|
|
46
48
|
throw invalidSchemaPayload(`Collection ${key} must be a boolean`);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
if (Object.hasOwn(patch, 'name')) {
|
|
52
|
+
patch.name = normalizeDisplayName(patch.name);
|
|
53
|
+
}
|
|
49
54
|
if (Object.hasOwn(patch, 'note') && patch.note != null && typeof patch.note !== 'string') {
|
|
50
55
|
throw invalidSchemaPayload('Collection note must be a string or null');
|
|
51
56
|
}
|
|
@@ -70,6 +75,7 @@ function assertCollectionCreateMetadata(input) {
|
|
|
70
75
|
)) {
|
|
71
76
|
throw invalidSchemaPayload('Collection metadata must be an object or null');
|
|
72
77
|
}
|
|
78
|
+
normalizeCollectionSystemFields(input.systemFields);
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
function temporaryDropName() {
|
|
@@ -91,7 +97,13 @@ export class CollectionsService extends BaseService {
|
|
|
91
97
|
async createOne(input = {}) {
|
|
92
98
|
assertSchemaManager(this.accountability);
|
|
93
99
|
assertCollectionCreateMetadata(input);
|
|
94
|
-
const
|
|
100
|
+
const resolvedName = resolveSchemaName({
|
|
101
|
+
displayName: input.name ?? input.collection,
|
|
102
|
+
key: input.collection,
|
|
103
|
+
prefix: 'collection',
|
|
104
|
+
});
|
|
105
|
+
const collection = assertUserCollectionName(resolvedName.key);
|
|
106
|
+
const name = resolvedName.name;
|
|
95
107
|
const primaryKey = input.primaryKey ?? 'id';
|
|
96
108
|
|
|
97
109
|
if (primaryKey !== 'id') {
|
|
@@ -100,6 +112,8 @@ export class CollectionsService extends BaseService {
|
|
|
100
112
|
throw error;
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
const systemFields = compileCollectionSystemFields(collection, input.systemFields);
|
|
116
|
+
|
|
103
117
|
return withAdvisoryLock(this.database, 'yuncms:schema', async (connection) => {
|
|
104
118
|
const metadata = new SchemaMetadataRepository(connection);
|
|
105
119
|
const existing = await metadata.readCollection(collection);
|
|
@@ -111,11 +125,16 @@ export class CollectionsService extends BaseService {
|
|
|
111
125
|
|
|
112
126
|
const table = quoteIdentifier(collection, 'collection name');
|
|
113
127
|
let tableCreated = false;
|
|
128
|
+
const physicalDefinitions = [
|
|
129
|
+
'id CHAR(36) NOT NULL PRIMARY KEY',
|
|
130
|
+
...systemFields.columns,
|
|
131
|
+
...systemFields.constraints,
|
|
132
|
+
];
|
|
114
133
|
|
|
115
134
|
try {
|
|
116
135
|
await connection.query(
|
|
117
136
|
`CREATE TABLE ${table} (
|
|
118
|
-
|
|
137
|
+
${physicalDefinitions.join(',\n ')}
|
|
119
138
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
120
139
|
);
|
|
121
140
|
tableCreated = true;
|
|
@@ -123,16 +142,21 @@ export class CollectionsService extends BaseService {
|
|
|
123
142
|
return await withConnectionTransaction(connection, async () => {
|
|
124
143
|
const created = await metadata.createCollection({
|
|
125
144
|
collection,
|
|
145
|
+
name,
|
|
126
146
|
primaryKey,
|
|
127
147
|
note: input.note ?? null,
|
|
128
148
|
singleton: input.singleton === true,
|
|
129
149
|
hidden: input.hidden === true,
|
|
130
|
-
metadata:
|
|
150
|
+
metadata: {
|
|
151
|
+
...(input.metadata ?? {}),
|
|
152
|
+
systemFields: systemFields.fields,
|
|
153
|
+
},
|
|
131
154
|
});
|
|
132
155
|
|
|
133
156
|
await metadata.createField({
|
|
134
157
|
collection,
|
|
135
158
|
field: 'id',
|
|
159
|
+
name: 'ID',
|
|
136
160
|
type: 'uuid',
|
|
137
161
|
required: true,
|
|
138
162
|
readonly: true,
|
|
@@ -140,6 +164,10 @@ export class CollectionsService extends BaseService {
|
|
|
140
164
|
schemaMetadata: { primaryKey: true, length: 36 },
|
|
141
165
|
});
|
|
142
166
|
|
|
167
|
+
for (const systemField of systemFields.metadata) {
|
|
168
|
+
await metadata.createField(systemField);
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
const schemaVersion = await incrementSchemaVersion(connection);
|
|
144
172
|
return { ...created, schemaVersion };
|
|
145
173
|
});
|
|
@@ -11,6 +11,7 @@ import { PermissionsService } from './permissions-service.js';
|
|
|
11
11
|
import { RelationsService } from './relations-service.js';
|
|
12
12
|
import { RolesService } from './roles-service.js';
|
|
13
13
|
import { StudioSettingsService } from './studio-settings-service.js';
|
|
14
|
+
import { SystemCollectionFieldsService } from './system-collection-fields-service.js';
|
|
14
15
|
import { UsersService } from './users-service.js';
|
|
15
16
|
import { createServiceRegistry } from './service-registry.js';
|
|
16
17
|
|
|
@@ -23,6 +24,7 @@ export function createCoreServiceRegistry() {
|
|
|
23
24
|
ItemsService,
|
|
24
25
|
CollectionsService,
|
|
25
26
|
FieldsService,
|
|
27
|
+
SystemCollectionFieldsService,
|
|
26
28
|
RelationsService,
|
|
27
29
|
UsersService,
|
|
28
30
|
RolesService,
|
|
@@ -4,13 +4,22 @@ import { withAdvisoryLock } from '../advisory-lock.js';
|
|
|
4
4
|
import { compileFieldColumn } from '../field-types.js';
|
|
5
5
|
import { assertIdentifier, quoteIdentifier } from '../identifier.js';
|
|
6
6
|
import { SchemaMetadataRepository } from '../schema-metadata-repository.js';
|
|
7
|
+
import { normalizeDisplayName, resolveSchemaName } from '../schema-key.js';
|
|
7
8
|
import { incrementSchemaVersion } from '../schema-version.js';
|
|
8
9
|
import { withConnectionTransaction } from '../transaction.js';
|
|
9
10
|
import { BaseService } from './base-service.js';
|
|
10
11
|
import { assertSchemaManager } from './schema-access.js';
|
|
11
12
|
|
|
12
|
-
const FIELD_METADATA_KEYS = new Set(['readonly', 'hidden', 'sort', 'interface', 'options']);
|
|
13
|
-
const FIELD_PHYSICAL_KEYS = new Set([
|
|
13
|
+
const FIELD_METADATA_KEYS = new Set(['name', 'readonly', 'hidden', 'sort', 'interface', 'options']);
|
|
14
|
+
const FIELD_PHYSICAL_KEYS = new Set([
|
|
15
|
+
'required',
|
|
16
|
+
'defaultValue',
|
|
17
|
+
'removeDefault',
|
|
18
|
+
'defaultPreset',
|
|
19
|
+
'removeDefaultPreset',
|
|
20
|
+
'autoUpdate',
|
|
21
|
+
'indexed',
|
|
22
|
+
]);
|
|
14
23
|
|
|
15
24
|
function assertFieldName(field) {
|
|
16
25
|
assertIdentifier(field, 'field name');
|
|
@@ -18,11 +27,15 @@ function assertFieldName(field) {
|
|
|
18
27
|
return field;
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
function invalidSchemaPayload(message) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
function assertFieldMetadataPatch(patch) {
|
|
22
37
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
23
|
-
|
|
24
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
25
|
-
throw error;
|
|
38
|
+
throw invalidSchemaPayload('Field metadata patch must be an object');
|
|
26
39
|
}
|
|
27
40
|
for (const key of Object.keys(patch)) {
|
|
28
41
|
if (!FIELD_METADATA_KEYS.has(key)) {
|
|
@@ -32,23 +45,18 @@ function assertFieldMetadataPatch(patch) {
|
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
if (Object.keys(patch).length === 0) {
|
|
35
|
-
|
|
36
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
37
|
-
throw error;
|
|
48
|
+
throw invalidSchemaPayload('Field metadata patch cannot be empty');
|
|
38
49
|
}
|
|
50
|
+
if (Object.hasOwn(patch, 'name')) patch.name = normalizeDisplayName(patch.name);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
function assertPhysicalPatch(patch) {
|
|
42
54
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
43
|
-
|
|
44
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
45
|
-
throw error;
|
|
55
|
+
throw invalidSchemaPayload('Physical field patch must be an object');
|
|
46
56
|
}
|
|
47
57
|
const keys = Object.keys(patch);
|
|
48
58
|
if (keys.length === 0) {
|
|
49
|
-
|
|
50
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
51
|
-
throw error;
|
|
59
|
+
throw invalidSchemaPayload('Physical field patch cannot be empty');
|
|
52
60
|
}
|
|
53
61
|
for (const key of keys) {
|
|
54
62
|
if (!FIELD_PHYSICAL_KEYS.has(key)) {
|
|
@@ -58,24 +66,30 @@ function assertPhysicalPatch(patch) {
|
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
68
|
if (Object.hasOwn(patch, 'required') && typeof patch.required !== 'boolean') {
|
|
61
|
-
|
|
62
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
63
|
-
throw error;
|
|
69
|
+
throw invalidSchemaPayload('required must be boolean');
|
|
64
70
|
}
|
|
65
71
|
if (Object.hasOwn(patch, 'indexed') && typeof patch.indexed !== 'boolean') {
|
|
66
|
-
|
|
67
|
-
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
68
|
-
throw error;
|
|
72
|
+
throw invalidSchemaPayload('indexed must be boolean');
|
|
69
73
|
}
|
|
70
|
-
if (Object.hasOwn(patch, '
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
if (Object.hasOwn(patch, 'autoUpdate') && typeof patch.autoUpdate !== 'boolean') {
|
|
75
|
+
throw invalidSchemaPayload('autoUpdate must be boolean');
|
|
76
|
+
}
|
|
77
|
+
for (const key of ['removeDefault', 'removeDefaultPreset']) {
|
|
78
|
+
if (Object.hasOwn(patch, key) && patch[key] !== true) {
|
|
79
|
+
throw invalidSchemaPayload(`${key} must be true when provided`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (Object.hasOwn(patch, 'defaultPreset') && patch.defaultPreset !== 'now') {
|
|
83
|
+
throw invalidSchemaPayload('defaultPreset currently supports only now');
|
|
84
|
+
}
|
|
85
|
+
if (Object.hasOwn(patch, 'defaultValue') && Object.hasOwn(patch, 'defaultPreset')) {
|
|
86
|
+
throw invalidSchemaPayload('defaultValue and defaultPreset cannot be used together');
|
|
74
87
|
}
|
|
75
88
|
if (Object.hasOwn(patch, 'defaultValue') && patch.removeDefault === true) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
throw invalidSchemaPayload('defaultValue and removeDefault cannot be used together');
|
|
90
|
+
}
|
|
91
|
+
if (Object.hasOwn(patch, 'defaultPreset') && patch.removeDefaultPreset === true) {
|
|
92
|
+
throw invalidSchemaPayload('defaultPreset and removeDefaultPreset cannot be used together');
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -89,15 +103,26 @@ function parseSchemaMetadata(value) {
|
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
function assertFieldNotSystemManaged(field) {
|
|
107
|
+
if (parseSchemaMetadata(field?.schema_metadata).systemManaged === true) {
|
|
108
|
+
const error = new Error(`System-managed field cannot be changed directly: ${field.collection}.${field.field}`);
|
|
109
|
+
error.code = 'SYSTEM_SCHEMA_READ_ONLY';
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
function fieldInputFromMetadata(field, schemaMetadata) {
|
|
93
115
|
const input = {
|
|
94
116
|
type: field.type,
|
|
95
117
|
required: Boolean(field.required),
|
|
118
|
+
interface: field.interface ?? null,
|
|
96
119
|
};
|
|
97
120
|
if (schemaMetadata.length !== undefined && field.type === 'string') input.length = schemaMetadata.length;
|
|
98
121
|
if (schemaMetadata.precision !== undefined && field.type === 'decimal') input.precision = schemaMetadata.precision;
|
|
99
122
|
if (schemaMetadata.scale !== undefined && field.type === 'decimal') input.scale = schemaMetadata.scale;
|
|
100
123
|
if (Object.hasOwn(schemaMetadata, 'defaultValue')) input.defaultValue = schemaMetadata.defaultValue;
|
|
124
|
+
if (schemaMetadata.defaultPreset !== undefined) input.defaultPreset = schemaMetadata.defaultPreset;
|
|
125
|
+
if (schemaMetadata.autoUpdate === true) input.autoUpdate = true;
|
|
101
126
|
return input;
|
|
102
127
|
}
|
|
103
128
|
|
|
@@ -127,7 +152,13 @@ export class FieldsService extends BaseService {
|
|
|
127
152
|
async createOne(collection, input = {}) {
|
|
128
153
|
assertSchemaManager(this.accountability);
|
|
129
154
|
assertIdentifier(collection, 'collection name');
|
|
130
|
-
const
|
|
155
|
+
const resolvedName = resolveSchemaName({
|
|
156
|
+
displayName: input.name ?? input.field,
|
|
157
|
+
key: input.field,
|
|
158
|
+
prefix: 'field',
|
|
159
|
+
});
|
|
160
|
+
const field = assertFieldName(resolvedName.key);
|
|
161
|
+
const name = resolvedName.name;
|
|
131
162
|
|
|
132
163
|
if (field === 'id') {
|
|
133
164
|
const error = new Error('The id field is created with the collection and cannot be added again');
|
|
@@ -173,6 +204,7 @@ export class FieldsService extends BaseService {
|
|
|
173
204
|
const created = await metadata.createField({
|
|
174
205
|
collection,
|
|
175
206
|
field,
|
|
207
|
+
name,
|
|
176
208
|
type: input.type,
|
|
177
209
|
required: input.required === true,
|
|
178
210
|
readonly: input.readonly === true,
|
|
@@ -246,6 +278,7 @@ export class FieldsService extends BaseService {
|
|
|
246
278
|
error.code = 'FIELD_NOT_FOUND';
|
|
247
279
|
throw error;
|
|
248
280
|
}
|
|
281
|
+
assertFieldNotSystemManaged(existing);
|
|
249
282
|
|
|
250
283
|
return withConnectionTransaction(connection, async () => {
|
|
251
284
|
const updated = await metadata.updateFieldMetadata(collection, field, patch);
|
|
@@ -288,6 +321,7 @@ export class FieldsService extends BaseService {
|
|
|
288
321
|
error.code = 'FIELD_NOT_FOUND';
|
|
289
322
|
throw error;
|
|
290
323
|
}
|
|
324
|
+
assertFieldNotSystemManaged(existing);
|
|
291
325
|
|
|
292
326
|
const relation = relations.find((candidate) =>
|
|
293
327
|
candidate.many_collection === collection && candidate.many_field === field);
|
|
@@ -301,8 +335,18 @@ export class FieldsService extends BaseService {
|
|
|
301
335
|
const currentMetadata = parseSchemaMetadata(existing.schema_metadata);
|
|
302
336
|
const currentInput = fieldInputFromMetadata(existing, currentMetadata);
|
|
303
337
|
const nextInput = { ...currentInput, required: nextRequired };
|
|
338
|
+
|
|
304
339
|
if (patch.removeDefault === true) delete nextInput.defaultValue;
|
|
305
|
-
|
|
340
|
+
if (patch.removeDefaultPreset === true) delete nextInput.defaultPreset;
|
|
341
|
+
if (Object.hasOwn(patch, 'defaultValue')) {
|
|
342
|
+
nextInput.defaultValue = patch.defaultValue;
|
|
343
|
+
delete nextInput.defaultPreset;
|
|
344
|
+
}
|
|
345
|
+
if (Object.hasOwn(patch, 'defaultPreset')) {
|
|
346
|
+
nextInput.defaultPreset = patch.defaultPreset;
|
|
347
|
+
delete nextInput.defaultValue;
|
|
348
|
+
}
|
|
349
|
+
if (Object.hasOwn(patch, 'autoUpdate')) nextInput.autoUpdate = patch.autoUpdate;
|
|
306
350
|
|
|
307
351
|
const currentCompiled = compileFieldColumn(currentInput);
|
|
308
352
|
const nextCompiled = compileFieldColumn(nextInput);
|
|
@@ -410,6 +454,7 @@ export class FieldsService extends BaseService {
|
|
|
410
454
|
error.code = 'FIELD_NOT_FOUND';
|
|
411
455
|
throw error;
|
|
412
456
|
}
|
|
457
|
+
assertFieldNotSystemManaged(existing);
|
|
413
458
|
|
|
414
459
|
const blockingRelation = relations.find((relation) =>
|
|
415
460
|
(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]);
|