@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/src/query.js CHANGED
@@ -7,6 +7,18 @@ const FILTER_OPERATORS = new Set([
7
7
  '_contains', '_starts_with', '_ends_with',
8
8
  ]);
9
9
 
10
+ export const QUERY_LIMITS = Object.freeze({
11
+ defaultLimit: 100,
12
+ maxLimit: 500,
13
+ maxFields: 100,
14
+ maxRelationExpansions: 20,
15
+ maxSortFields: 20,
16
+ maxOffset: 1_000_000,
17
+ maxFilterDepth: 8,
18
+ maxFilterNodes: 100,
19
+ maxInValues: 100,
20
+ });
21
+
10
22
  function queryError(message, path = null) {
11
23
  const error = new Error(message);
12
24
  error.code = 'INVALID_QUERY';
@@ -14,11 +26,14 @@ function queryError(message, path = null) {
14
26
  return error;
15
27
  }
16
28
 
17
- function normalizeDelimited(value, label) {
29
+ function normalizeDelimited(value, label, { maxItems }) {
18
30
  if (value == null || value === '') return null;
19
31
  const values = Array.isArray(value) ? value : String(value).split(',');
20
32
  const normalized = values.map((item) => String(item).trim()).filter(Boolean);
21
33
  if (normalized.length === 0) throw queryError(`${label} cannot be empty`, label);
34
+ if (normalized.length > maxItems) {
35
+ throw queryError(`${label} cannot contain more than ${maxItems} entries`, label);
36
+ }
22
37
  return normalized;
23
38
  }
24
39
 
@@ -50,7 +65,8 @@ function normalizeFilter(value) {
50
65
  return value;
51
66
  }
52
67
 
53
- export function parseItemsQuery(raw = {}, { defaultLimit = 100, maxLimit = 500 } = {}) {
68
+ export function parseItemsQuery(raw = {}, options = {}) {
69
+ const limits = { ...QUERY_LIMITS, ...options };
54
70
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
55
71
  throw queryError('Query must be an object');
56
72
  }
@@ -60,11 +76,19 @@ export function parseItemsQuery(raw = {}, { defaultLimit = 100, maxLimit = 500 }
60
76
  }
61
77
 
62
78
  return {
63
- fields: normalizeDelimited(raw.fields, 'fields'),
79
+ fields: normalizeDelimited(raw.fields, 'fields', { maxItems: limits.maxFields }),
64
80
  filter: normalizeFilter(raw.filter),
65
- sort: normalizeDelimited(raw.sort, 'sort'),
66
- limit: normalizeInteger(raw.limit, defaultLimit, { label: 'limit', min: 1, max: maxLimit }),
67
- offset: normalizeInteger(raw.offset, 0, { label: 'offset', min: 0, max: Number.MAX_SAFE_INTEGER }),
81
+ sort: normalizeDelimited(raw.sort, 'sort', { maxItems: limits.maxSortFields }),
82
+ limit: normalizeInteger(raw.limit, limits.defaultLimit, {
83
+ label: 'limit',
84
+ min: 1,
85
+ max: limits.maxLimit,
86
+ }),
87
+ offset: normalizeInteger(raw.offset, 0, {
88
+ label: 'offset',
89
+ min: 0,
90
+ max: limits.maxOffset,
91
+ }),
68
92
  };
69
93
  }
70
94
 
@@ -105,7 +129,7 @@ function escapeLike(value) {
105
129
  return String(value).replace(/[\\%_]/g, '\\$&');
106
130
  }
107
131
 
108
- function compileOperator(fieldSql, operator, value, path) {
132
+ function compileOperator(fieldSql, operator, value, path, limits) {
109
133
  if (!FILTER_OPERATORS.has(operator)) throw queryError(`Unknown filter operator: ${operator}`, path);
110
134
 
111
135
  switch (operator) {
@@ -122,6 +146,9 @@ function compileOperator(fieldSql, operator, value, path) {
122
146
  case '_in':
123
147
  case '_nin': {
124
148
  if (!Array.isArray(value)) throw queryError(`${operator} requires an array`, path);
149
+ if (value.length > limits.maxInValues) {
150
+ throw queryError(`${operator} accepts at most ${limits.maxInValues} values`, path);
151
+ }
125
152
  if (value.length === 0) return { sql: operator === '_in' ? '0 = 1' : '1 = 1', params: [] };
126
153
  const placeholders = value.map(() => '?').join(', ');
127
154
  return {
@@ -146,10 +173,18 @@ function compileOperator(fieldSql, operator, value, path) {
146
173
  }
147
174
  }
148
175
 
149
- function compileFilterObject(filter, schema, path = 'filter') {
176
+ function compileFilterObject(filter, schema, path, limits, state, depth) {
150
177
  if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
151
178
  throw queryError('Filter node must be an object', path);
152
179
  }
180
+ if (depth > limits.maxFilterDepth) {
181
+ throw queryError(`Filter depth cannot exceed ${limits.maxFilterDepth}`, path);
182
+ }
183
+
184
+ state.nodes += 1;
185
+ if (state.nodes > limits.maxFilterNodes) {
186
+ throw queryError(`Filter cannot contain more than ${limits.maxFilterNodes} nodes`, path);
187
+ }
153
188
 
154
189
  const fragments = [];
155
190
  const params = [];
@@ -160,7 +195,7 @@ function compileFilterObject(filter, schema, path = 'filter') {
160
195
  throw queryError(`${key} requires a non-empty array`, `${path}.${key}`);
161
196
  }
162
197
  const children = value.map((child, index) =>
163
- compileFilterObject(child, schema, `${path}.${key}.${index}`));
198
+ compileFilterObject(child, schema, `${path}.${key}.${index}`, limits, state, depth + 1));
164
199
  fragments.push(`(${children.map((child) => child.sql).join(key === '_and' ? ' AND ' : ' OR ')})`);
165
200
  for (const child of children) params.push(...child.params);
166
201
  continue;
@@ -174,7 +209,13 @@ function compileFilterObject(filter, schema, path = 'filter') {
174
209
  const fieldSql = quoteIdentifier(key, 'field name');
175
210
  const fieldFragments = [];
176
211
  for (const [operator, operatorValue] of Object.entries(value)) {
177
- const compiled = compileOperator(fieldSql, operator, operatorValue, `${path}.${key}.${operator}`);
212
+ const compiled = compileOperator(
213
+ fieldSql,
214
+ operator,
215
+ operatorValue,
216
+ `${path}.${key}.${operator}`,
217
+ limits,
218
+ );
178
219
  fieldFragments.push(compiled.sql);
179
220
  params.push(...compiled.params);
180
221
  }
@@ -186,8 +227,9 @@ function compileFilterObject(filter, schema, path = 'filter') {
186
227
  return { sql: fragments.join(' AND '), params };
187
228
  }
188
229
 
189
- export function compileFilter(filter, schema) {
230
+ export function compileFilter(filter, schema, options = {}) {
190
231
  if (!filter) return { sql: '', params: [] };
191
- const compiled = compileFilterObject(filter, schema);
232
+ const limits = { ...QUERY_LIMITS, ...options };
233
+ const compiled = compileFilterObject(filter, schema, 'filter', limits, { nodes: 0 }, 1);
192
234
  return { sql: ` WHERE ${compiled.sql}`, params: compiled.params };
193
235
  }
@@ -1,8 +1,9 @@
1
1
  import { assertIdentifier } from './identifier.js';
2
+ import { QUERY_LIMITS } from './query.js';
2
3
  import { SchemaCache } from './schema.js';
3
4
  import { ItemsService } from './services/items-service.js';
4
5
 
5
- const MAX_EXPAND_FIELDS = 8;
6
+ export const MAX_EXPAND_FIELDS = QUERY_LIMITS.maxRelationExpansions;
6
7
  const defaultSchemaCache = new SchemaCache();
7
8
 
8
9
  function expansionError(code, message, path = null) {
@@ -18,16 +19,24 @@ function normalizeDelimited(value) {
18
19
  return values.map((entry) => String(entry).trim()).filter(Boolean);
19
20
  }
20
21
 
22
+ function assertFieldToken(field, path) {
23
+ try {
24
+ assertIdentifier(field, 'field');
25
+ } catch {
26
+ throw expansionError('INVALID_QUERY', `Invalid field: ${field}`, path);
27
+ }
28
+ return field;
29
+ }
30
+
21
31
  export function parseExpandInput(value) {
22
32
  const fields = [...new Set(normalizeDelimited(value))];
23
33
  if (fields.length > MAX_EXPAND_FIELDS) {
24
34
  throw expansionError(
25
35
  'INVALID_QUERY',
26
- `expand supports at most ${MAX_EXPAND_FIELDS} direct relation fields`,
36
+ `expand cannot contain more than ${MAX_EXPAND_FIELDS} entries`,
27
37
  'expand',
28
38
  );
29
39
  }
30
-
31
40
  for (const field of fields) {
32
41
  try {
33
42
  assertIdentifier(field, 'expand field');
@@ -47,73 +56,199 @@ function parseRelationMetadata(value) {
47
56
  }
48
57
  }
49
58
 
50
- function withExpansionFields(rawFields, expandFields) {
51
- if (rawFields == null || rawFields === '') return rawFields;
52
- const selected = normalizeDelimited(rawFields);
53
- if (selected.includes('*')) return selected;
54
- return [...new Set([...selected, ...expandFields])];
55
- }
56
-
57
- function withoutExpand(query = {}, expandFields = []) {
58
- const base = { ...query };
59
- delete base.expand;
60
- if (expandFields.length > 0 && Object.hasOwn(base, 'fields')) {
61
- base.fields = withExpansionFields(base.fields, expandFields);
62
- }
63
- return base;
64
- }
65
-
66
59
  async function schemaSnapshot(options) {
67
60
  if (options.schema) return options.schema;
68
61
  const cache = options.schemaCache ?? defaultSchemaCache;
69
62
  return cache.get(options.database);
70
63
  }
71
64
 
72
- function directRelation(snapshot, collection, field) {
73
- const relation = snapshot.relationByManyField?.get(`${collection}.${field}`);
65
+ function relationFromSnapshot(snapshot, collection, field) {
66
+ return snapshot.relationByManyField?.get(`${collection}.${field}`) ?? null;
67
+ }
68
+
69
+ function isDirectRelation(relation) {
70
+ if (!relation) return false;
71
+ const metadata = parseRelationMetadata(relation.metadata);
72
+ return !relation.junction_collection && metadata.kind !== 'm2m';
73
+ }
74
+
75
+ function directRelation(snapshot, collection, field, path = `fields.${field}`) {
76
+ const relation = relationFromSnapshot(snapshot, collection, field);
74
77
  if (!relation) {
75
78
  throw expansionError(
76
79
  'INVALID_QUERY',
77
80
  `Field is not a direct relation and cannot be expanded: ${collection}.${field}`,
78
- `expand.${field}`,
81
+ path,
79
82
  );
80
83
  }
81
84
 
82
- const metadata = parseRelationMetadata(relation.metadata);
83
- if (relation.junction_collection || metadata.kind === 'm2m') {
85
+ if (!isDirectRelation(relation)) {
84
86
  throw expansionError(
85
87
  'UNSUPPORTED_RELATION_EXPANSION',
86
- `Only direct M2O fields can be expanded in V1: ${collection}.${field}`,
87
- `expand.${field}`,
88
+ `Only direct M2O/O2O fields can be expanded: ${collection}.${field}`,
89
+ path,
88
90
  );
89
91
  }
90
92
  return relation;
91
93
  }
92
94
 
93
- async function validateSourceExpansions({ collection, expandFields, options, service }) {
95
+ function readableSourceField(sourceSchema, permission, field) {
96
+ return Boolean(sourceSchema.fields?.[field]
97
+ && (!permission.fields || permission.fields.includes(field)));
98
+ }
99
+
100
+ function directReadableRelationFields(snapshot, collection, sourceSchema, permission) {
101
+ const prefix = `${collection}.`;
102
+ const fields = [];
103
+ for (const [key, relation] of snapshot.relationByManyField?.entries?.() ?? []) {
104
+ if (!key.startsWith(prefix) || !isDirectRelation(relation)) continue;
105
+ const field = key.slice(prefix.length);
106
+ if (readableSourceField(sourceSchema, permission, field)) fields.push(field);
107
+ }
108
+ return [...new Set(fields)];
109
+ }
110
+
111
+ function mergeExpansionSelection(expansions, relationField, targetField) {
112
+ const current = expansions.get(relationField) ?? [];
113
+ if (current.includes('*')) return;
114
+ if (targetField === '*') {
115
+ expansions.set(relationField, ['*']);
116
+ return;
117
+ }
118
+ expansions.set(relationField, [...new Set([...current, targetField])]);
119
+ }
120
+
121
+ function assertExpansionLimit(expansions, path = 'fields') {
122
+ if (expansions.size > MAX_EXPAND_FIELDS) {
123
+ throw expansionError(
124
+ 'INVALID_QUERY',
125
+ `Relation expansion cannot contain more than ${MAX_EXPAND_FIELDS} fields`,
126
+ path,
127
+ );
128
+ }
129
+ }
130
+
131
+ function parseFieldsPlan({ value, snapshot, collection, sourceSchema, permission }) {
132
+ const tokens = normalizeDelimited(value);
133
+ if (tokens.length === 0) {
134
+ return { sourceFields: null, expansions: new Map() };
135
+ }
136
+
137
+ let sourceAll = false;
138
+ const sourceFields = [];
139
+ const expansions = new Map();
140
+
141
+ for (const token of tokens) {
142
+ if (token === '*') {
143
+ sourceAll = true;
144
+ continue;
145
+ }
146
+
147
+ if (token === '*.*') {
148
+ sourceAll = true;
149
+ for (const relationField of directReadableRelationFields(
150
+ snapshot,
151
+ collection,
152
+ sourceSchema,
153
+ permission,
154
+ )) {
155
+ mergeExpansionSelection(expansions, relationField, '*');
156
+ }
157
+ assertExpansionLimit(expansions);
158
+ continue;
159
+ }
160
+
161
+ if (!token.includes('.')) {
162
+ sourceFields.push(assertFieldToken(token, `fields.${token}`));
163
+ continue;
164
+ }
165
+
166
+ const parts = token.split('.');
167
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
168
+ throw expansionError(
169
+ 'UNSUPPORTED_RELATION_EXPANSION',
170
+ `Only one relation depth is supported in fields: ${token}`,
171
+ `fields.${token}`,
172
+ );
173
+ }
174
+
175
+ const relationField = assertFieldToken(parts[0], `fields.${token}`);
176
+ const targetField = parts[1] === '*'
177
+ ? '*'
178
+ : assertFieldToken(parts[1], `fields.${token}`);
179
+
180
+ if (!readableSourceField(sourceSchema, permission, relationField)) {
181
+ throw expansionError('INVALID_QUERY', `Unknown field: ${relationField}`, `fields.${token}`);
182
+ }
183
+ directRelation(snapshot, collection, relationField, `fields.${token}`);
184
+ sourceFields.push(relationField);
185
+ mergeExpansionSelection(expansions, relationField, targetField);
186
+ assertExpansionLimit(expansions);
187
+ }
188
+
189
+ return {
190
+ sourceFields: sourceAll ? ['*'] : [...new Set(sourceFields)],
191
+ expansions,
192
+ };
193
+ }
194
+
195
+ function addLegacyExpansions({ query, plan, snapshot, collection, sourceSchema, permission }) {
196
+ for (const field of parseExpandInput(query.expand)) {
197
+ if (!readableSourceField(sourceSchema, permission, field)) {
198
+ throw expansionError('INVALID_QUERY', `Unknown field: ${field}`, `expand.${field}`);
199
+ }
200
+ directRelation(snapshot, collection, field, `expand.${field}`);
201
+ mergeExpansionSelection(plan.expansions, field, '*');
202
+ assertExpansionLimit(plan.expansions, 'expand');
203
+ if (plan.sourceFields && !plan.sourceFields.includes('*') && !plan.sourceFields.includes(field)) {
204
+ plan.sourceFields.push(field);
205
+ }
206
+ }
207
+ return plan;
208
+ }
209
+
210
+ async function buildSelectionPlan({ collection, query, options, service }) {
94
211
  const snapshot = await schemaSnapshot(options);
95
212
  const sourceSchema = snapshot.collections?.[collection];
96
213
  if (!sourceSchema) {
97
214
  throw expansionError('COLLECTION_NOT_FOUND', `Unknown collection: ${collection}`);
98
215
  }
99
- if (expandFields.length === 0) return snapshot;
100
216
 
101
217
  const permission = await service.resolvePermission('read');
102
- for (const field of expandFields) {
103
- if (!sourceSchema.fields?.[field] || (permission.fields && !permission.fields.includes(field))) {
104
- throw expansionError('INVALID_QUERY', `Unknown field: ${field}`, `expand.${field}`);
105
- }
106
- directRelation(snapshot, collection, field);
107
- }
108
- return snapshot;
218
+ const plan = parseFieldsPlan({
219
+ value: query.fields,
220
+ snapshot,
221
+ collection,
222
+ sourceSchema,
223
+ permission,
224
+ });
225
+ addLegacyExpansions({ query, plan, snapshot, collection, sourceSchema, permission });
226
+ return { ...plan, snapshot };
109
227
  }
110
228
 
111
- async function expandRows({ collection, rows, expandFields, options, ItemsServiceClass, snapshot }) {
112
- if (expandFields.length === 0 || rows.length === 0) return rows;
229
+ function baseQuery(query, sourceFields) {
230
+ const base = { ...query };
231
+ delete base.expand;
232
+ if (sourceFields == null) delete base.fields;
233
+ else base.fields = sourceFields;
234
+ return base;
235
+ }
236
+
237
+ function projectTarget(target, selection, visibleFields) {
238
+ const projectedFields = selection.includes('*') ? visibleFields : selection;
239
+ return Object.fromEntries(
240
+ projectedFields
241
+ .filter((field) => Object.hasOwn(target, field))
242
+ .map((field) => [field, target[field]]),
243
+ );
244
+ }
245
+
246
+ async function expandRows({ collection, rows, expansions, options, ItemsServiceClass, snapshot }) {
247
+ if (expansions.size === 0 || rows.length === 0) return rows;
113
248
  const effectiveSnapshot = snapshot ?? await schemaSnapshot(options);
114
249
  let expandedRows = rows.map((row) => ({ ...row }));
115
250
 
116
- for (const field of expandFields) {
251
+ for (const [field, selection] of expansions) {
117
252
  const relation = directRelation(effectiveSnapshot, collection, field);
118
253
  const targetCollection = relation.one_collection;
119
254
  const targetKey = relation.one_field
@@ -132,21 +267,25 @@ async function expandRows({ collection, rows, expandFields, options, ItemsServic
132
267
  }
133
268
 
134
269
  const targetService = new ItemsServiceClass(targetCollection, options);
135
- const targetRows = await targetService.readMany({
136
- filter: { [targetKey]: { _in: values } },
137
- limit: Math.min(values.length, 500),
270
+ const targetResult = await targetService.readManyForRelation({
271
+ fields: selection,
272
+ lookupField: targetKey,
273
+ values,
138
274
  });
139
275
 
140
276
  const byKey = new Map();
141
- for (const target of targetRows) {
277
+ for (const target of targetResult.data) {
142
278
  if (!Object.hasOwn(target, targetKey)) {
143
279
  throw expansionError(
144
280
  'FORBIDDEN_FIELD',
145
281
  `Expanded relation key is not readable: ${targetCollection}.${targetKey}`,
146
- `expand.${field}`,
282
+ `fields.${field}`,
147
283
  );
148
284
  }
149
- byKey.set(String(target[targetKey]), target);
285
+ byKey.set(
286
+ String(target[targetKey]),
287
+ projectTarget(target, selection, targetResult.visibleFields),
288
+ );
150
289
  }
151
290
 
152
291
  expandedRows = expandedRows.map((row) => ({
@@ -167,17 +306,16 @@ export async function readManyWithRelations({
167
306
  ItemsServiceClass = ItemsService,
168
307
  } = {}) {
169
308
  assertIdentifier(collection, 'collection name');
170
- const expandFields = parseExpandInput(query.expand);
171
309
  const service = new ItemsServiceClass(collection, options);
172
- const snapshot = await validateSourceExpansions({ collection, expandFields, options, service });
173
- const result = await service.readManyWithMeta(withoutExpand(query, expandFields));
310
+ const plan = await buildSelectionPlan({ collection, query, options, service });
311
+ const result = await service.readManyWithMeta(baseQuery(query, plan.sourceFields));
174
312
  const data = await expandRows({
175
313
  collection,
176
314
  rows: result.data,
177
- expandFields,
315
+ expansions: plan.expansions,
178
316
  options,
179
317
  ItemsServiceClass,
180
- snapshot,
318
+ snapshot: plan.snapshot,
181
319
  });
182
320
  return { ...result, data };
183
321
  }
@@ -196,21 +334,17 @@ export async function readOneWithRelations({
196
334
  }
197
335
  }
198
336
 
199
- const expandFields = parseExpandInput(query.expand);
200
337
  const service = new ItemsServiceClass(collection, options);
201
- const snapshot = await validateSourceExpansions({ collection, expandFields, options, service });
202
- const fields = withExpansionFields(query.fields ?? null, expandFields);
203
- const record = await service.readOne(id, { fields });
338
+ const plan = await buildSelectionPlan({ collection, query, options, service });
339
+ const record = await service.readOne(id, { fields: plan.sourceFields });
204
340
  if (!record) return null;
205
341
  const [expanded] = await expandRows({
206
342
  collection,
207
343
  rows: [record],
208
- expandFields,
344
+ expansions: plan.expansions,
209
345
  options,
210
346
  ItemsServiceClass,
211
- snapshot,
347
+ snapshot: plan.snapshot,
212
348
  });
213
349
  return expanded;
214
350
  }
215
-
216
- export { MAX_EXPAND_FIELDS };
@@ -0,0 +1,54 @@
1
+ const TURKISH_ASCII = Object.freeze({
2
+ ç: 'c',
3
+ Ç: 'c',
4
+ ğ: 'g',
5
+ Ğ: 'g',
6
+ ı: 'i',
7
+ İ: 'i',
8
+ ö: 'o',
9
+ Ö: 'o',
10
+ ş: 's',
11
+ Ş: 's',
12
+ ü: 'u',
13
+ Ü: 'u',
14
+ });
15
+
16
+ function schemaNameError(message) {
17
+ const error = new Error(message);
18
+ error.code = 'INVALID_SCHEMA_NAME';
19
+ return error;
20
+ }
21
+
22
+ export function normalizeDisplayName(value, { fallback = null, maxLength = 255 } = {}) {
23
+ const candidate = typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
24
+ const normalized = candidate || (typeof fallback === 'string' ? fallback.trim().replace(/\s+/g, ' ') : '');
25
+ if (!normalized) throw schemaNameError('A display name is required');
26
+ if (normalized.length > maxLength) throw schemaNameError(`Display name cannot exceed ${maxLength} characters`);
27
+ return normalized;
28
+ }
29
+
30
+ export function normalizeSchemaKey(value, { prefix = 'field', maxLength = 64 } = {}) {
31
+ const source = normalizeDisplayName(value, { maxLength: 512 });
32
+ const transliterated = [...source].map((character) => TURKISH_ASCII[character] ?? character).join('');
33
+ let key = transliterated
34
+ .normalize('NFKD')
35
+ .replace(/[\u0300-\u036f]/g, '')
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, '_')
38
+ .replace(/^_+|_+$/g, '')
39
+ .replace(/_+/g, '_');
40
+
41
+ if (!key) throw schemaNameError('Name must contain at least one letter or number');
42
+ if (/^[0-9]/.test(key)) key = `${prefix}_${key}`;
43
+ if (key.length > maxLength) key = key.slice(0, maxLength).replace(/_+$/g, '');
44
+ if (!key) throw schemaNameError('Name cannot be normalized to a schema key');
45
+ return key;
46
+ }
47
+
48
+ export function resolveSchemaName({ displayName, key, prefix = 'field' } = {}) {
49
+ const name = normalizeDisplayName(displayName ?? key);
50
+ return {
51
+ name,
52
+ key: normalizeSchemaKey(key ?? name, { prefix }),
53
+ };
54
+ }
@@ -12,7 +12,7 @@ export class SchemaMetadataRepository {
12
12
 
13
13
  async listCollections() {
14
14
  const [rows] = await this.database.query(
15
- `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
16
16
  FROM yuncms_collections
17
17
  ORDER BY collection ASC`,
18
18
  );
@@ -21,7 +21,7 @@ export class SchemaMetadataRepository {
21
21
 
22
22
  async readCollection(collection) {
23
23
  const [rows] = await this.database.query(
24
- `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
25
25
  FROM yuncms_collections
26
26
  WHERE collection = ?
27
27
  LIMIT 1`,
@@ -32,6 +32,7 @@ export class SchemaMetadataRepository {
32
32
 
33
33
  async createCollection({
34
34
  collection,
35
+ name = collection,
35
36
  primaryKey = 'id',
36
37
  note = null,
37
38
  singleton = false,
@@ -41,10 +42,11 @@ export class SchemaMetadataRepository {
41
42
  }) {
42
43
  await this.database.query(
43
44
  `INSERT INTO yuncms_collections
44
- (collection, primary_key, note, singleton, hidden, \`system\`, metadata)
45
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
45
+ (collection, name, primary_key, note, singleton, hidden, \`system\`, metadata)
46
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
46
47
  [
47
48
  collection,
49
+ name,
48
50
  primaryKey,
49
51
  note,
50
52
  singleton ? 1 : 0,
@@ -60,6 +62,10 @@ export class SchemaMetadataRepository {
60
62
  const assignments = [];
61
63
  const params = [];
62
64
 
65
+ if (Object.hasOwn(patch, 'name')) {
66
+ assignments.push('name = ?');
67
+ params.push(patch.name);
68
+ }
63
69
  if (Object.hasOwn(patch, 'note')) {
64
70
  assignments.push('note = ?');
65
71
  params.push(patch.note ?? null);
@@ -96,7 +102,7 @@ export class SchemaMetadataRepository {
96
102
 
97
103
  async listFields(collection) {
98
104
  const [rows] = await this.database.query(
99
- `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,
100
106
  created_at, updated_at
101
107
  FROM yuncms_fields
102
108
  WHERE collection = ?
@@ -108,7 +114,7 @@ export class SchemaMetadataRepository {
108
114
 
109
115
  async readField(collection, field) {
110
116
  const [rows] = await this.database.query(
111
- `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,
112
118
  created_at, updated_at
113
119
  FROM yuncms_fields
114
120
  WHERE collection = ? AND field = ?
@@ -121,6 +127,7 @@ export class SchemaMetadataRepository {
121
127
  async createField({
122
128
  collection,
123
129
  field,
130
+ name = field,
124
131
  type,
125
132
  required = false,
126
133
  readonly = false,
@@ -137,11 +144,12 @@ export class SchemaMetadataRepository {
137
144
  }
138
145
  await this.database.query(
139
146
  `INSERT INTO yuncms_fields
140
- (collection, field, type, required, readonly, hidden, sort, interface, options, schema_metadata)
141
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
147
+ (collection, field, name, type, required, readonly, hidden, sort, interface, options, schema_metadata)
148
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
142
149
  [
143
150
  collection,
144
151
  field,
152
+ name,
145
153
  type,
146
154
  required ? 1 : 0,
147
155
  readonly ? 1 : 0,
@@ -169,6 +177,10 @@ export class SchemaMetadataRepository {
169
177
  }
170
178
  }
171
179
 
180
+ if (Object.hasOwn(patch, 'name')) {
181
+ assignments.push('name = ?');
182
+ params.push(patch.name);
183
+ }
172
184
  if (Object.hasOwn(patch, 'readonly')) {
173
185
  assignments.push('readonly = ?');
174
186
  params.push(patch.readonly ? 1 : 0);