@yunsoft/yuncms-core 0.1.2 → 0.1.5
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/package.json +1 -1
- package/src/bootstrap.js +10 -0
- package/src/cache.js +85 -0
- package/src/config.js +50 -0
- package/src/context.js +5 -2
- package/src/index.js +12 -1
- package/src/maintenance-state.js +89 -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/migrations/0011-role-permission-actions.js +15 -0
- package/src/migrations/0012-files-read-filters.js +14 -0
- package/src/migrations.js +113 -3
- package/src/query.js +54 -12
- package/src/relation-expansion.js +191 -57
- package/src/schema-key.js +54 -0
- package/src/schema-metadata-repository.js +20 -8
- package/src/services/collections-service.js +14 -2
- package/src/services/core-services.js +2 -0
- package/src/services/fields-service.js +11 -2
- package/src/services/files-service.js +85 -7
- package/src/services/items-service.js +54 -1
- package/src/services/permissions-service.js +35 -15
- package/src/services/roles-service.js +44 -23
- package/src/services/studio-settings-service.js +83 -11
- package/src/services/system-collection-fields-service.js +122 -0
- package/src/services/users-service.js +27 -3
- package/src/system-permissions.js +50 -7
|
@@ -3,6 +3,11 @@ import { BaseService } from './base-service.js';
|
|
|
3
3
|
const THEMES = new Set(['system', 'light', 'dark']);
|
|
4
4
|
const LOCALES = new Set(['en', 'tr']);
|
|
5
5
|
const ACCENT_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
6
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7
|
+
const YUNSOFT_LOGOS = new Set([
|
|
8
|
+
'https://yunsoft.com/light-logo.png',
|
|
9
|
+
'https://yunsoft.com/dark-logo.png',
|
|
10
|
+
]);
|
|
6
11
|
|
|
7
12
|
function invalid(message) {
|
|
8
13
|
const error = new Error(message);
|
|
@@ -10,6 +15,12 @@ function invalid(message) {
|
|
|
10
15
|
return error;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
function notFound(message) {
|
|
19
|
+
const error = new Error(message);
|
|
20
|
+
error.code = 'NOT_FOUND';
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function assertManager(accountability) {
|
|
14
25
|
if (accountability.admin === true || accountability.system === true) return;
|
|
15
26
|
const error = new Error('Studio settings require administrator accountability');
|
|
@@ -25,16 +36,17 @@ function normalizeBrandName(value) {
|
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
function normalizeLogoUrl(value) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let url;
|
|
32
|
-
try {
|
|
33
|
-
url = new URL(normalized);
|
|
34
|
-
} catch {
|
|
35
|
-
throw invalid('Logo URL must be a valid URL');
|
|
39
|
+
const normalized = String(value ?? '').trim();
|
|
40
|
+
if (!YUNSOFT_LOGOS.has(normalized)) {
|
|
41
|
+
throw invalid('External logo URLs are not supported; choose an image from Files');
|
|
36
42
|
}
|
|
37
|
-
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeImageFile(value, label) {
|
|
47
|
+
if (value == null || value === '') return null;
|
|
48
|
+
const normalized = String(value).trim();
|
|
49
|
+
if (!UUID_PATTERN.test(normalized)) throw invalid(`${label} file id must be a UUID`);
|
|
38
50
|
return normalized;
|
|
39
51
|
}
|
|
40
52
|
|
|
@@ -60,6 +72,8 @@ function publicSettings(row) {
|
|
|
60
72
|
return {
|
|
61
73
|
brand_name: row.brand_name,
|
|
62
74
|
logo_url: row.logo_url,
|
|
75
|
+
logo_file: row.logo_file ?? null,
|
|
76
|
+
favicon_file: row.favicon_file ?? null,
|
|
63
77
|
accent_color: row.accent_color,
|
|
64
78
|
theme: row.theme,
|
|
65
79
|
default_locale: row.default_locale,
|
|
@@ -70,7 +84,7 @@ function publicSettings(row) {
|
|
|
70
84
|
export class StudioSettingsService extends BaseService {
|
|
71
85
|
async readPublic() {
|
|
72
86
|
const [rows] = await this.database.query(
|
|
73
|
-
`SELECT brand_name, logo_url, accent_color, theme, default_locale, updated_at
|
|
87
|
+
`SELECT brand_name, logo_url, logo_file, favicon_file, accent_color, theme, default_locale, updated_at
|
|
74
88
|
FROM yuncms_studio_settings
|
|
75
89
|
WHERE id = 1
|
|
76
90
|
LIMIT 1`,
|
|
@@ -89,12 +103,56 @@ export class StudioSettingsService extends BaseService {
|
|
|
89
103
|
return this.readPublic();
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
async readImageAssetContent(settingKey, label) {
|
|
107
|
+
const settings = await this.readPublic();
|
|
108
|
+
const fileId = settings[settingKey];
|
|
109
|
+
if (!fileId) throw notFound(`No file-backed Studio ${label.toLowerCase()} is configured`);
|
|
110
|
+
if (!this.storage) throw new Error(`StudioSettingsService requires storage to read a file-backed ${label.toLowerCase()}`);
|
|
111
|
+
|
|
112
|
+
const [rows] = await this.database.query(
|
|
113
|
+
`SELECT id, storage, filename_disk, mimetype, filesize
|
|
114
|
+
FROM yuncms_files
|
|
115
|
+
WHERE id = ?
|
|
116
|
+
LIMIT 1`,
|
|
117
|
+
[fileId],
|
|
118
|
+
);
|
|
119
|
+
const file = rows[0];
|
|
120
|
+
if (!file) throw notFound(`Configured Studio ${label.toLowerCase()} file does not exist`);
|
|
121
|
+
if (!String(file.mimetype || '').toLowerCase().startsWith('image/')) {
|
|
122
|
+
throw invalid(`Configured Studio ${label.toLowerCase()} must be an image`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const driver = this.storage.get(file.storage);
|
|
126
|
+
const contents = await driver.get(file.filename_disk);
|
|
127
|
+
return { file, contents };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async readLogoContent() {
|
|
131
|
+
return this.readImageAssetContent('logo_file', 'Logo');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async readFaviconContent() {
|
|
135
|
+
return this.readImageAssetContent('favicon_file', 'Favicon');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async validateSelectedImage(fileId, label) {
|
|
139
|
+
if (!fileId) return;
|
|
140
|
+
const [files] = await this.database.query(
|
|
141
|
+
'SELECT id, mimetype FROM yuncms_files WHERE id = ? LIMIT 1',
|
|
142
|
+
[fileId],
|
|
143
|
+
);
|
|
144
|
+
if (!files[0]) throw invalid(`Selected ${label.toLowerCase()} file does not exist`);
|
|
145
|
+
if (!String(files[0].mimetype || '').toLowerCase().startsWith('image/')) {
|
|
146
|
+
throw invalid(`Selected ${label.toLowerCase()} file must be an image`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
92
150
|
async updateOne(patch = {}) {
|
|
93
151
|
assertManager(this.accountability);
|
|
94
152
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw invalid('Studio settings patch must be an object');
|
|
95
153
|
|
|
96
154
|
const keys = Object.keys(patch);
|
|
97
|
-
const allowed = new Set(['brand_name', 'logo_url', 'accent_color', 'theme', 'default_locale']);
|
|
155
|
+
const allowed = new Set(['brand_name', 'logo_url', 'logo_file', 'favicon_file', 'accent_color', 'theme', 'default_locale']);
|
|
98
156
|
if (keys.length === 0 || keys.some((key) => !allowed.has(key))) {
|
|
99
157
|
throw invalid('Studio settings patch contains unsupported properties');
|
|
100
158
|
}
|
|
@@ -109,6 +167,18 @@ export class StudioSettingsService extends BaseService {
|
|
|
109
167
|
assignments.push('logo_url = ?');
|
|
110
168
|
params.push(normalizeLogoUrl(patch.logo_url));
|
|
111
169
|
}
|
|
170
|
+
if (Object.hasOwn(patch, 'logo_file')) {
|
|
171
|
+
const fileId = normalizeImageFile(patch.logo_file, 'Logo');
|
|
172
|
+
await this.validateSelectedImage(fileId, 'Logo');
|
|
173
|
+
assignments.push('logo_file = ?');
|
|
174
|
+
params.push(fileId);
|
|
175
|
+
}
|
|
176
|
+
if (Object.hasOwn(patch, 'favicon_file')) {
|
|
177
|
+
const fileId = normalizeImageFile(patch.favicon_file, 'Favicon');
|
|
178
|
+
await this.validateSelectedImage(fileId, 'Favicon');
|
|
179
|
+
assignments.push('favicon_file = ?');
|
|
180
|
+
params.push(fileId);
|
|
181
|
+
}
|
|
112
182
|
if (Object.hasOwn(patch, 'accent_color')) {
|
|
113
183
|
assignments.push('accent_color = ?');
|
|
114
184
|
params.push(normalizeAccent(patch.accent_color));
|
|
@@ -134,6 +204,8 @@ export class StudioSettingsService extends BaseService {
|
|
|
134
204
|
export const STUDIO_SETTING_DEFAULTS = Object.freeze({
|
|
135
205
|
brand_name: 'YunCMS',
|
|
136
206
|
logo_url: 'https://yunsoft.com/light-logo.png',
|
|
207
|
+
logo_file: null,
|
|
208
|
+
favicon_file: null,
|
|
137
209
|
accent_color: '#2563eb',
|
|
138
210
|
theme: 'system',
|
|
139
211
|
default_locale: 'en',
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { withAdvisoryLock } from '../advisory-lock.js';
|
|
2
|
+
import { compileFieldColumn } from '../field-types.js';
|
|
3
|
+
import { assertIdentifier, quoteIdentifier } from '../identifier.js';
|
|
4
|
+
import { SchemaMetadataRepository } from '../schema-metadata-repository.js';
|
|
5
|
+
import { resolveSchemaName } from '../schema-key.js';
|
|
6
|
+
import { incrementSchemaVersion } from '../schema-version.js';
|
|
7
|
+
import { isPermissionManagedSystemResource } from '../system-permissions.js';
|
|
8
|
+
import { withConnectionTransaction } from '../transaction.js';
|
|
9
|
+
import { BaseService } from './base-service.js';
|
|
10
|
+
import { assertSchemaManager } from './schema-access.js';
|
|
11
|
+
|
|
12
|
+
function fieldName(value) {
|
|
13
|
+
assertIdentifier(value, 'field name');
|
|
14
|
+
if (value.length > 64) {
|
|
15
|
+
const error = new Error('Field name cannot exceed 64 characters');
|
|
16
|
+
error.code = 'INVALID_SCHEMA_PAYLOAD';
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
if (value === 'id') {
|
|
20
|
+
const error = new Error('The id field cannot be replaced');
|
|
21
|
+
error.code = 'FIELD_EXISTS';
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertExtensibleSystemCollection(collection) {
|
|
28
|
+
if (!collection?.system || !isPermissionManagedSystemResource(collection)) {
|
|
29
|
+
const error = new Error(`System collection is not extensible: ${collection?.collection || 'unknown'}`);
|
|
30
|
+
error.code = 'SYSTEM_SCHEMA_READ_ONLY';
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertSystemExtensionInput(input = {}) {
|
|
36
|
+
if (input.required === true) {
|
|
37
|
+
const error = new Error('Custom system collection fields must be optional in V1');
|
|
38
|
+
error.code = 'SYSTEM_EXTENSION_REQUIRED_UNSUPPORTED';
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class SystemCollectionFieldsService extends BaseService {
|
|
44
|
+
async createOne(collectionName, input = {}) {
|
|
45
|
+
assertSchemaManager(this.accountability);
|
|
46
|
+
assertIdentifier(collectionName, 'collection name');
|
|
47
|
+
assertSystemExtensionInput(input);
|
|
48
|
+
const resolvedName = resolveSchemaName({
|
|
49
|
+
displayName: input.name ?? input.field,
|
|
50
|
+
key: input.field,
|
|
51
|
+
prefix: 'field',
|
|
52
|
+
});
|
|
53
|
+
const field = fieldName(resolvedName.key);
|
|
54
|
+
const name = resolvedName.name;
|
|
55
|
+
const compiled = compileFieldColumn({ ...input, required: false });
|
|
56
|
+
|
|
57
|
+
return withAdvisoryLock(this.database, 'yuncms:schema', async (connection) => {
|
|
58
|
+
const metadata = new SchemaMetadataRepository(connection);
|
|
59
|
+
const collection = await metadata.readCollection(collectionName);
|
|
60
|
+
if (!collection) {
|
|
61
|
+
const error = new Error(`Unknown collection: ${collectionName}`);
|
|
62
|
+
error.code = 'COLLECTION_NOT_FOUND';
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
assertExtensibleSystemCollection(collection);
|
|
66
|
+
|
|
67
|
+
const existing = await metadata.readField(collectionName, field);
|
|
68
|
+
if (existing) {
|
|
69
|
+
const error = new Error(`Field already exists: ${collectionName}.${field}`);
|
|
70
|
+
error.code = 'FIELD_EXISTS';
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const table = quoteIdentifier(collectionName, 'collection name');
|
|
75
|
+
const column = quoteIdentifier(field, 'field name');
|
|
76
|
+
let physicalCreated = false;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await connection.query(`ALTER TABLE ${table} ADD COLUMN ${column} ${compiled.sql}`, compiled.params);
|
|
80
|
+
physicalCreated = true;
|
|
81
|
+
|
|
82
|
+
return await withConnectionTransaction(connection, async () => {
|
|
83
|
+
const created = await metadata.createField({
|
|
84
|
+
collection: collectionName,
|
|
85
|
+
field,
|
|
86
|
+
name,
|
|
87
|
+
type: input.type,
|
|
88
|
+
required: false,
|
|
89
|
+
readonly: input.readonly === true,
|
|
90
|
+
hidden: input.hidden === true,
|
|
91
|
+
sort: input.sort ?? null,
|
|
92
|
+
interface: input.interface ?? null,
|
|
93
|
+
options: input.options ?? null,
|
|
94
|
+
schemaMetadata: {
|
|
95
|
+
...compiled.schemaMetadata,
|
|
96
|
+
systemExtension: true,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const schemaVersion = await incrementSchemaVersion(connection);
|
|
100
|
+
return { ...created, schemaVersion };
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
try {
|
|
104
|
+
await metadata.deleteField(collectionName, field);
|
|
105
|
+
} catch (cleanupError) {
|
|
106
|
+
error.cleanupMetadataError = cleanupError;
|
|
107
|
+
}
|
|
108
|
+
if (physicalCreated) {
|
|
109
|
+
try {
|
|
110
|
+
await connection.query(`ALTER TABLE ${table} DROP COLUMN ${column}`);
|
|
111
|
+
} catch (cleanupError) {
|
|
112
|
+
error.cleanupPhysicalError = cleanupError;
|
|
113
|
+
error.code ||= 'SCHEMA_PARTIAL_FAILURE';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { assertExtensibleSystemCollection, assertSystemExtensionInput };
|
|
@@ -63,6 +63,13 @@ async function assertRoleAssignable(database, role, accountability) {
|
|
|
63
63
|
if (targetRole.admin && accountability.admin !== true && accountability.system !== true) {
|
|
64
64
|
throw forbidden('Only an administrator can assign the administrator role');
|
|
65
65
|
}
|
|
66
|
+
if (
|
|
67
|
+
accountability.admin !== true
|
|
68
|
+
&& accountability.system !== true
|
|
69
|
+
&& role !== accountability.role
|
|
70
|
+
) {
|
|
71
|
+
throw forbidden('Delegated user managers may assign only their own role');
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
async function assertTargetManageable(database, id, accountability) {
|
|
@@ -84,6 +91,15 @@ async function assertTargetManageable(database, id, accountability) {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
export class UsersService extends BaseService {
|
|
94
|
+
async action(event, payload) {
|
|
95
|
+
if (!this.emitter) return;
|
|
96
|
+
await this.emitter.action(event, payload, {
|
|
97
|
+
accountability: this.accountability,
|
|
98
|
+
requestId: this.requestId,
|
|
99
|
+
collection: 'yuncms_users',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
async #readOneUnsafe(id) {
|
|
88
104
|
const [rows] = await this.database.query(
|
|
89
105
|
`SELECT id, email, role, status, email_verified_at, last_access, created_at, updated_at
|
|
@@ -133,7 +149,9 @@ export class UsersService extends BaseService {
|
|
|
133
149
|
],
|
|
134
150
|
);
|
|
135
151
|
|
|
136
|
-
|
|
152
|
+
const user = await this.#readOneUnsafe(id);
|
|
153
|
+
await this.action('users.create', { key: id, item: user });
|
|
154
|
+
return user;
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
async updateOne(id, patch = {}) {
|
|
@@ -163,7 +181,7 @@ export class UsersService extends BaseService {
|
|
|
163
181
|
await assertRoleAssignable(this.database, patch.role, this.accountability);
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
|
|
184
|
+
const user = await withTransaction(this.database, async (connection) => {
|
|
167
185
|
const assignments = [];
|
|
168
186
|
const params = [];
|
|
169
187
|
|
|
@@ -202,6 +220,8 @@ export class UsersService extends BaseService {
|
|
|
202
220
|
);
|
|
203
221
|
return rows[0] ?? null;
|
|
204
222
|
});
|
|
223
|
+
await this.action('users.update', { key: id, item: user, changes: patch });
|
|
224
|
+
return user;
|
|
205
225
|
}
|
|
206
226
|
|
|
207
227
|
async deleteOne(id) {
|
|
@@ -212,12 +232,14 @@ export class UsersService extends BaseService {
|
|
|
212
232
|
error.code = 'SELF_ADMIN_MUTATION_FORBIDDEN';
|
|
213
233
|
throw error;
|
|
214
234
|
}
|
|
235
|
+
const before = this.emitter ? await this.#readOneUnsafe(id) : null;
|
|
215
236
|
const [result] = await this.database.query('DELETE FROM yuncms_users WHERE id = ?', [id]);
|
|
216
237
|
if (result.affectedRows !== 1) {
|
|
217
238
|
const error = new Error(`Unknown user: ${id}`);
|
|
218
239
|
error.code = 'USER_NOT_FOUND';
|
|
219
240
|
throw error;
|
|
220
241
|
}
|
|
242
|
+
await this.action('users.delete', { key: id, before });
|
|
221
243
|
return true;
|
|
222
244
|
}
|
|
223
245
|
|
|
@@ -241,7 +263,6 @@ export class UsersService extends BaseService {
|
|
|
241
263
|
}
|
|
242
264
|
await connection.query('DELETE FROM yuncms_sessions WHERE user = ?', [id]);
|
|
243
265
|
await connection.commit();
|
|
244
|
-
return true;
|
|
245
266
|
} catch (error) {
|
|
246
267
|
try {
|
|
247
268
|
await connection.rollback();
|
|
@@ -252,6 +273,9 @@ export class UsersService extends BaseService {
|
|
|
252
273
|
} finally {
|
|
253
274
|
connection.release();
|
|
254
275
|
}
|
|
276
|
+
|
|
277
|
+
await this.action('users.password.update', { key: id });
|
|
278
|
+
return true;
|
|
255
279
|
}
|
|
256
280
|
}
|
|
257
281
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ALL_ACTIONS = Object.freeze(['read', 'create', 'update', 'delete']);
|
|
2
|
+
const PERMISSION_MODES = new Set(['action-only', 'filter-read']);
|
|
2
3
|
|
|
3
4
|
function parseMetadata(value) {
|
|
4
5
|
if (value == null) return {};
|
|
@@ -14,16 +15,28 @@ function isEnabledFlag(value) {
|
|
|
14
15
|
return value === true || value === 1;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function advancedUnsupported(collectionSchema, message) {
|
|
19
|
+
const error = new Error(message ?? `System resource ${collectionSchema.collection} does not support this advanced permission rule`);
|
|
20
|
+
error.code = 'SYSTEM_PERMISSION_ADVANCED_UNSUPPORTED';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export function systemPermissionConfig(collectionSchema) {
|
|
18
25
|
if (!collectionSchema?.system) return null;
|
|
19
26
|
const metadata = parseMetadata(collectionSchema.metadata);
|
|
20
27
|
if (!isEnabledFlag(metadata.permissionManaged)) return null;
|
|
28
|
+
const mode = metadata.permissionMode ?? 'action-only';
|
|
29
|
+
if (!PERMISSION_MODES.has(mode)) {
|
|
30
|
+
const error = new Error(`Unsupported system permission mode for ${collectionSchema.collection}: ${mode}`);
|
|
31
|
+
error.code = 'SYSTEM_PERMISSION_MODE_UNSUPPORTED';
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
21
34
|
const allowedActions = Array.isArray(metadata.allowedActions)
|
|
22
35
|
? metadata.allowedActions.filter((action) => ALL_ACTIONS.includes(action))
|
|
23
36
|
: [];
|
|
24
37
|
return Object.freeze({
|
|
25
38
|
resource: metadata.resource ?? collectionSchema.collection,
|
|
26
|
-
mode
|
|
39
|
+
mode,
|
|
27
40
|
allowedActions: Object.freeze([...new Set(allowedActions)]),
|
|
28
41
|
});
|
|
29
42
|
}
|
|
@@ -43,12 +56,42 @@ export function assertSystemResourceAction(collectionSchema, action) {
|
|
|
43
56
|
return config;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
export function
|
|
59
|
+
export function assertSystemPermissionPayload(
|
|
60
|
+
collectionSchema,
|
|
61
|
+
action,
|
|
62
|
+
{ fields, filter, validation } = {},
|
|
63
|
+
) {
|
|
47
64
|
const config = systemPermissionConfig(collectionSchema);
|
|
48
|
-
if (!config
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
if (!config) return;
|
|
66
|
+
|
|
67
|
+
if (config.mode === 'action-only') {
|
|
68
|
+
if (fields != null || filter != null || validation != null) {
|
|
69
|
+
advancedUnsupported(
|
|
70
|
+
collectionSchema,
|
|
71
|
+
`System resource ${collectionSchema.collection} supports action-level permissions only`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
53
75
|
}
|
|
76
|
+
|
|
77
|
+
if (config.mode === 'filter-read') {
|
|
78
|
+
if (fields != null || validation != null) {
|
|
79
|
+
advancedUnsupported(
|
|
80
|
+
collectionSchema,
|
|
81
|
+
`System resource ${collectionSchema.collection} supports only a row filter on read permissions`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (filter != null && action !== 'read') {
|
|
85
|
+
advancedUnsupported(
|
|
86
|
+
collectionSchema,
|
|
87
|
+
`System resource ${collectionSchema.collection} permits row filters only for read`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function assertActionOnlyPermissionPayload(collectionSchema, payload = {}) {
|
|
94
|
+
const config = systemPermissionConfig(collectionSchema);
|
|
95
|
+
if (!config || config.mode !== 'action-only') return;
|
|
96
|
+
return assertSystemPermissionPayload(collectionSchema, 'read', payload);
|
|
54
97
|
}
|