@kollors/deep-json-server 1.0.0-alpha.5 → 1.0.0-alpha.7

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.
Files changed (215) hide show
  1. package/README.md +346 -188
  2. package/README.ru.md +343 -185
  3. package/dist/bin/deep-json-server.js +1 -1
  4. package/dist/bin/deep-json-server.js.map +1 -1
  5. package/dist/index.d.ts +12 -8
  6. package/dist/index.js +4 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +27 -0
  9. package/dist/src/auth/contract.js +5 -0
  10. package/dist/src/auth/contract.js.map +1 -0
  11. package/dist/src/auth/password.d.ts +13 -0
  12. package/dist/src/auth/password.js +37 -0
  13. package/dist/src/auth/password.js.map +1 -0
  14. package/dist/src/auth/public.d.ts +3 -0
  15. package/dist/src/auth/public.js +5 -0
  16. package/dist/src/auth/public.js.map +1 -0
  17. package/dist/src/auth/routes.d.ts +6 -0
  18. package/dist/src/auth/routes.js +15 -0
  19. package/dist/src/auth/routes.js.map +1 -0
  20. package/dist/src/auth/service.d.ts +39 -0
  21. package/dist/src/auth/service.js +143 -0
  22. package/dist/src/auth/service.js.map +1 -0
  23. package/dist/src/cli/index.d.ts +7 -0
  24. package/dist/src/{cli.js → cli/index.js} +32 -12
  25. package/dist/src/cli/index.js.map +1 -0
  26. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -1
  27. package/dist/src/{constants.js → core/constants.js} +1 -1
  28. package/dist/src/core/constants.js.map +1 -0
  29. package/dist/src/core/database.d.ts +50 -0
  30. package/dist/src/{database.js → core/database.js} +21 -1
  31. package/dist/src/core/database.js.map +1 -0
  32. package/dist/src/core/engine.d.ts +64 -0
  33. package/dist/src/{engine.js → core/engine.js} +64 -18
  34. package/dist/src/core/engine.js.map +1 -0
  35. package/dist/src/core/errors.d.ts +9 -0
  36. package/dist/src/core/errors.js +12 -0
  37. package/dist/src/core/errors.js.map +1 -0
  38. package/dist/src/core/http-errors.d.ts +9 -0
  39. package/dist/src/core/http-errors.js +15 -0
  40. package/dist/src/core/http-errors.js.map +1 -0
  41. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  42. package/dist/src/core/lifecycle/mutation.js +170 -0
  43. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  44. package/dist/src/core/lifecycle/options.d.ts +20 -0
  45. package/dist/src/core/lifecycle/options.js +21 -0
  46. package/dist/src/core/lifecycle/options.js.map +1 -0
  47. package/dist/src/core/model.d.ts +130 -0
  48. package/dist/src/{model.js → core/model.js} +106 -10
  49. package/dist/src/core/model.js.map +1 -0
  50. package/dist/src/{mutations → core/mutations}/write.d.ts +3 -2
  51. package/dist/src/{mutations → core/mutations}/write.js +14 -4
  52. package/dist/src/core/mutations/write.js.map +1 -0
  53. package/dist/src/core/pagination.d.ts +10 -0
  54. package/dist/src/{pagination.js → core/pagination.js} +3 -0
  55. package/dist/src/core/pagination.js.map +1 -0
  56. package/dist/src/core/paths.d.ts +16 -0
  57. package/dist/src/{paths.js → core/paths.js} +14 -1
  58. package/dist/src/core/paths.js.map +1 -0
  59. package/dist/src/core/query/contract.d.ts +6 -0
  60. package/dist/src/{query → core/query}/contract.js +3 -0
  61. package/dist/src/core/query/contract.js.map +1 -0
  62. package/dist/src/core/query/filter.d.ts +6 -0
  63. package/dist/src/{query → core/query}/filter.js +18 -4
  64. package/dist/src/core/query/filter.js.map +1 -0
  65. package/dist/src/core/query/options.d.ts +30 -0
  66. package/dist/src/{query → core/query}/options.js +12 -0
  67. package/dist/src/core/query/options.js.map +1 -0
  68. package/dist/src/core/records.d.ts +46 -0
  69. package/dist/src/{records.js → core/records.js} +24 -3
  70. package/dist/src/core/records.js.map +1 -0
  71. package/dist/src/core/relation-metadata.d.ts +15 -0
  72. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -1
  73. package/dist/src/core/relation-metadata.js.map +1 -0
  74. package/dist/src/core/types.d.ts +10 -0
  75. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  76. package/dist/src/core/utils.d.ts +56 -0
  77. package/dist/src/core/utils.js +102 -0
  78. package/dist/src/core/utils.js.map +1 -0
  79. package/dist/src/files/contract.d.ts +33 -0
  80. package/dist/src/files/contract.js +25 -3
  81. package/dist/src/files/contract.js.map +1 -1
  82. package/dist/src/files/disk-store.d.ts +3 -0
  83. package/dist/src/files/disk-store.js +27 -3
  84. package/dist/src/files/disk-store.js.map +1 -1
  85. package/dist/src/files/http.d.ts +8 -3
  86. package/dist/src/files/http.js +9 -0
  87. package/dist/src/files/http.js.map +1 -1
  88. package/dist/src/files/index.d.ts +4 -3
  89. package/dist/src/files/index.js +3 -1
  90. package/dist/src/files/index.js.map +1 -1
  91. package/dist/src/files/memory-store.d.ts +4 -1
  92. package/dist/src/files/memory-store.js +8 -2
  93. package/dist/src/files/memory-store.js.map +1 -1
  94. package/dist/src/files/routes.d.ts +3 -0
  95. package/dist/src/files/routes.js +25 -1
  96. package/dist/src/files/routes.js.map +1 -1
  97. package/dist/src/graphql/entry.d.ts +2 -2
  98. package/dist/src/graphql/entry.js.map +1 -1
  99. package/dist/src/graphql/lazy.d.ts +10 -0
  100. package/dist/src/graphql/lazy.js +13 -0
  101. package/dist/src/graphql/lazy.js.map +1 -0
  102. package/dist/src/graphql/preflight.d.ts +4 -1
  103. package/dist/src/graphql/preflight.js +3 -0
  104. package/dist/src/graphql/preflight.js.map +1 -1
  105. package/dist/src/graphql/public.d.ts +10 -2
  106. package/dist/src/graphql/public.js +10 -4
  107. package/dist/src/graphql/public.js.map +1 -1
  108. package/dist/src/graphql/resolvers.d.ts +10 -2
  109. package/dist/src/graphql/resolvers.js +8 -2
  110. package/dist/src/graphql/resolvers.js.map +1 -1
  111. package/dist/src/graphql/routes.d.ts +6 -2
  112. package/dist/src/graphql/routes.js +6 -3
  113. package/dist/src/graphql/routes.js.map +1 -1
  114. package/dist/src/graphql/schema.d.ts +6 -0
  115. package/dist/src/{graphql.js → graphql/schema.js} +14 -11
  116. package/dist/src/graphql/schema.js.map +1 -0
  117. package/dist/src/graphql/write.d.ts +3 -0
  118. package/dist/src/graphql/write.js +3 -0
  119. package/dist/src/graphql/write.js.map +1 -1
  120. package/dist/src/openapi/auth.d.ts +13 -0
  121. package/dist/src/openapi/auth.js +72 -0
  122. package/dist/src/openapi/auth.js.map +1 -0
  123. package/dist/src/openapi/document.d.ts +7 -3
  124. package/dist/src/openapi/document.js +49 -34
  125. package/dist/src/openapi/document.js.map +1 -1
  126. package/dist/src/openapi/entry.d.ts +2 -2
  127. package/dist/src/openapi/entry.js.map +1 -1
  128. package/dist/src/openapi/files.d.ts +6 -0
  129. package/dist/src/{files/openapi.js → openapi/files.js} +6 -3
  130. package/dist/src/openapi/files.js.map +1 -0
  131. package/dist/src/openapi/helpers.d.ts +10 -1
  132. package/dist/src/openapi/helpers.js +9 -0
  133. package/dist/src/openapi/helpers.js.map +1 -1
  134. package/dist/src/openapi/index.d.ts +7 -1
  135. package/dist/src/openapi/index.js +14 -5
  136. package/dist/src/openapi/index.js.map +1 -1
  137. package/dist/src/openapi/lazy.d.ts +11 -0
  138. package/dist/src/openapi/lazy.js +13 -0
  139. package/dist/src/openapi/lazy.js.map +1 -0
  140. package/dist/src/openapi/public.d.ts +11 -2
  141. package/dist/src/openapi/public.js +12 -4
  142. package/dist/src/openapi/public.js.map +1 -1
  143. package/dist/src/{types.d.ts → openapi/types.d.ts} +2 -10
  144. package/dist/src/openapi/types.js +2 -0
  145. package/dist/src/openapi/types.js.map +1 -0
  146. package/dist/src/rest/options.d.ts +11 -2
  147. package/dist/src/rest/options.js +12 -3
  148. package/dist/src/rest/options.js.map +1 -1
  149. package/dist/src/rest/projection.d.ts +10 -4
  150. package/dist/src/rest/projection.js +10 -4
  151. package/dist/src/rest/projection.js.map +1 -1
  152. package/dist/src/rest/routes.d.ts +6 -2
  153. package/dist/src/rest/routes.js +7 -3
  154. package/dist/src/rest/routes.js.map +1 -1
  155. package/dist/src/server/config.d.ts +79 -0
  156. package/dist/src/{config.js → server/config.js} +73 -12
  157. package/dist/src/server/config.js.map +1 -0
  158. package/dist/src/server/create.d.ts +18 -0
  159. package/dist/src/{server.js → server/create.js} +39 -21
  160. package/dist/src/server/create.js.map +1 -0
  161. package/dist/src/server/features.d.ts +15 -0
  162. package/dist/src/server/features.js +31 -0
  163. package/dist/src/server/features.js.map +1 -0
  164. package/dist/src/server/public.d.ts +3 -3
  165. package/dist/src/server/public.js +1 -1
  166. package/dist/src/server/public.js.map +1 -1
  167. package/package.json +5 -1
  168. package/dist/src/cli.d.ts +0 -4
  169. package/dist/src/cli.js.map +0 -1
  170. package/dist/src/config.d.ts +0 -81
  171. package/dist/src/config.js.map +0 -1
  172. package/dist/src/constants.js.map +0 -1
  173. package/dist/src/database.d.ts +0 -20
  174. package/dist/src/database.js.map +0 -1
  175. package/dist/src/engine.d.ts +0 -36
  176. package/dist/src/engine.js.map +0 -1
  177. package/dist/src/errors.d.ts +0 -6
  178. package/dist/src/errors.js +0 -9
  179. package/dist/src/errors.js.map +0 -1
  180. package/dist/src/features.d.ts +0 -8
  181. package/dist/src/features.js +0 -18
  182. package/dist/src/features.js.map +0 -1
  183. package/dist/src/files/openapi.d.ts +0 -3
  184. package/dist/src/files/openapi.js.map +0 -1
  185. package/dist/src/graphql.d.ts +0 -3
  186. package/dist/src/graphql.js.map +0 -1
  187. package/dist/src/http/errors.d.ts +0 -6
  188. package/dist/src/http/errors.js +0 -3
  189. package/dist/src/http/errors.js.map +0 -1
  190. package/dist/src/model.d.ts +0 -72
  191. package/dist/src/model.js.map +0 -1
  192. package/dist/src/mutations/write.js.map +0 -1
  193. package/dist/src/pagination.d.ts +0 -7
  194. package/dist/src/pagination.js.map +0 -1
  195. package/dist/src/paths.d.ts +0 -6
  196. package/dist/src/paths.js.map +0 -1
  197. package/dist/src/query/contract.d.ts +0 -3
  198. package/dist/src/query/contract.js.map +0 -1
  199. package/dist/src/query/filter.d.ts +0 -3
  200. package/dist/src/query/filter.js.map +0 -1
  201. package/dist/src/query/options.d.ts +0 -18
  202. package/dist/src/query/options.js.map +0 -1
  203. package/dist/src/records.d.ts +0 -25
  204. package/dist/src/records.js.map +0 -1
  205. package/dist/src/relation-metadata.d.ts +0 -9
  206. package/dist/src/relation-metadata.js.map +0 -1
  207. package/dist/src/schema.d.ts +0 -8
  208. package/dist/src/schema.js +0 -13
  209. package/dist/src/schema.js.map +0 -1
  210. package/dist/src/server.d.ts +0 -12
  211. package/dist/src/server.js.map +0 -1
  212. package/dist/src/utils.d.ts +0 -12
  213. package/dist/src/utils.js +0 -58
  214. package/dist/src/utils.js.map +0 -1
  215. /package/dist/src/{types.js → core/types.js} +0 -0
@@ -0,0 +1,170 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { domainError } from '../errors.js';
3
+ import { isEqual, isObject } from '../utils.js';
4
+ import { DELETION_META } from './options.js';
5
+ const keyOf = (entity, record) => JSON.stringify([entity.collection, String(record[entity.primary])]);
6
+ /** Проверяет владельцев и заполняет поля дат и авторов внутри транзакции. */
7
+ export class RecordMutation {
8
+ data;
9
+ model;
10
+ actor;
11
+ before;
12
+ now = new Date().toISOString();
13
+ originals = new Map();
14
+ touched = new Set();
15
+ enabled;
16
+ constructor(data, model, actor) {
17
+ this.data = data;
18
+ this.model = model;
19
+ this.actor = actor;
20
+ if (model.options.auth && !actor)
21
+ throw domainError('UNAUTHENTICATED', 'Authentication required');
22
+ this.enabled = model.options.auth || model.entities.some((entity) => entity.timestamps || entity.softDelete);
23
+ this.before = this.enabled ? structuredClone(data) : data;
24
+ if (!this.enabled)
25
+ return;
26
+ for (const entity of model.entities)
27
+ for (const record of this.before[entity.collection] ?? [])
28
+ this.originals.set(keyOf(entity, record), record);
29
+ }
30
+ /** Проверяет владельца исходной записи или права администратора; данные не меняет.
31
+ * @example Совпадающие createdById и actor.id → undefined; чужая запись без прав администратора → FORBIDDEN.
32
+ */
33
+ check(entity, record) {
34
+ const original = this.originals.get(keyOf(entity, record));
35
+ if (this.model.options.auth && original && !this.actor?.isAdmin && (original.createdById == null || original.createdById !== this.actor?.id))
36
+ throw domainError('FORBIDDEN', 'Only the owner or an administrator can change this record');
37
+ }
38
+ /** Отмечает запись изменённой и при необходимости восстанавливает её каскад в черновике.
39
+ * @example Удалённая запись после успешного вызова → deletedAt: null; конфликт восстановления → ошибка.
40
+ */
41
+ write(entity, record) {
42
+ if (!this.enabled)
43
+ return;
44
+ this.check(entity, record);
45
+ this.touched.add(keyOf(entity, record));
46
+ if (!entity.softDelete || record.deletedAt == null)
47
+ return;
48
+ const deletion = this.deletion(record);
49
+ if (deletion?.root) {
50
+ for (const owner of this.model.entities)
51
+ for (const row of this.data[owner.collection] ?? []) {
52
+ if (!owner.softDelete || row.deletedAt == null || this.deletion(row)?.id !== deletion.id)
53
+ continue;
54
+ this.check(owner, row);
55
+ this.clear(owner, row);
56
+ }
57
+ for (const patch of deletion.patches ?? []) {
58
+ const owner = this.model.byCollection.get(patch.collection);
59
+ const row = owner && (this.data[owner.collection] ?? []).find((value) => String(value[owner.primary]) === String(patch.key));
60
+ if (!owner || !row || row.deletedAt != null || !isEqual(row[patch.field], patch.after))
61
+ throw domainError('CONFLICT', 'A cascaded object changed after deletion');
62
+ this.check(owner, row);
63
+ row[patch.field] = structuredClone(patch.before);
64
+ this.touched.add(keyOf(owner, row));
65
+ }
66
+ }
67
+ this.clear(entity, record);
68
+ }
69
+ /** Очищает признаки удаления и служебные сведения восстановления, отмечая запись изменённой.
70
+ * @example Запись с deletedAt и сведениями каскада → deletedAt: null без служебных сведений; результат undefined.
71
+ */
72
+ clear(entity, record) {
73
+ record.deletedAt = null;
74
+ if (this.model.options.auth)
75
+ record.deletedById = null;
76
+ delete record[DELETION_META];
77
+ this.touched.add(keyOf(entity, record));
78
+ }
79
+ /** Помечает выбранные записи временем удаления и общим случайным идентификатором операции.
80
+ * @example Две записи с включённым мягким удалением → одинаковые дата и идентификатор каскада.
81
+ */
82
+ deleteGroup(initial, deleted) {
83
+ const id = randomUUID();
84
+ for (const entity of this.model.entities)
85
+ for (const record of this.data[entity.collection] ?? []) {
86
+ if (!deleted.has(record) || !entity.softDelete)
87
+ continue;
88
+ this.check(entity, record);
89
+ record.deletedAt = this.now;
90
+ if (this.model.options.auth)
91
+ record.deletedById = this.actor?.id ?? null;
92
+ record[DELETION_META] = JSON.stringify({ id, root: record === initial.value });
93
+ }
94
+ }
95
+ /** Сохраняет изменения вложенных объектов для последующего восстановления каскада.
96
+ * @example Поле actors изменилось с [a, b] на [b] → в сведениях удаления сохранены оба состояния.
97
+ */
98
+ capturePruned(initial) {
99
+ if (!initial.entity.softDelete)
100
+ return;
101
+ const patches = [];
102
+ for (const entity of this.model.entities)
103
+ for (const row of this.data[entity.collection] ?? []) {
104
+ const old = this.originals.get(keyOf(entity, row));
105
+ if (!old || row.deletedAt != null)
106
+ continue;
107
+ for (const [field, node] of Object.entries(entity.root.children)) {
108
+ if (node.relation || node.base !== 'object' || !Object.hasOwn(old, field) || isEqual(old[field], row[field]))
109
+ continue;
110
+ patches.push({ collection: entity.collection, key: row[entity.primary], field, before: old[field], ...(row[field] !== undefined ? { after: structuredClone(row[field]) } : {}) });
111
+ }
112
+ }
113
+ const deletion = this.deletion(initial.value);
114
+ initial.value[DELETION_META] = JSON.stringify({ ...deletion, patches });
115
+ }
116
+ /** Проверяет права на все изменения и проставляет даты и авторов в черновике.
117
+ * @example Новая запись при включённых флагах → createdAt = updatedAt и createdById = updatedById.
118
+ */
119
+ finish() {
120
+ if (!this.enabled)
121
+ return;
122
+ for (const entity of this.model.entities) {
123
+ const present = new Set();
124
+ for (const record of this.data[entity.collection] ?? []) {
125
+ const key = keyOf(entity, record);
126
+ present.add(key);
127
+ const original = this.originals.get(key);
128
+ if (original && !this.touched.has(key) && isEqual(original, record))
129
+ continue;
130
+ this.check(entity, record);
131
+ for (const [name, field] of Object.entries(entity.root.children))
132
+ if (field.system && !field.internal && !Object.hasOwn(record, name))
133
+ record[name] = null;
134
+ if (entity.timestamps) {
135
+ record.createdAt = original?.createdAt ?? (original ? null : this.now);
136
+ record.updatedAt = this.now;
137
+ }
138
+ if (this.model.options.auth) {
139
+ record.createdById = original?.createdById ?? (original ? null : (this.actor?.id ?? null));
140
+ record.updatedById = this.actor?.id ?? null;
141
+ }
142
+ }
143
+ for (const old of this.before[entity.collection] ?? [])
144
+ if (!present.has(keyOf(entity, old)))
145
+ this.check(entity, old);
146
+ }
147
+ }
148
+ /** Разбирает и проверяет внутреннюю JSON-строку сведений об удалении.
149
+ * @example Отсутствующие сведения → undefined; корректная строка → объект; повреждённая строка → ошибка.
150
+ */
151
+ deletion(record) {
152
+ const raw = record[DELETION_META];
153
+ if (raw == null)
154
+ return undefined;
155
+ try {
156
+ const value = JSON.parse(raw);
157
+ if (!isObject(value) ||
158
+ typeof value.id !== 'string' ||
159
+ typeof value.root !== 'boolean' ||
160
+ (value.patches !== undefined &&
161
+ (!Array.isArray(value.patches) || value.patches.some((p) => !isObject(p) || typeof p.collection !== 'string' || typeof p.field !== 'string' || !Object.hasOwn(p, 'before')))))
162
+ throw new Error();
163
+ return value;
164
+ }
165
+ catch {
166
+ throw domainError('INVALID_INPUT', 'Invalid deletion metadata');
167
+ }
168
+ }
169
+ }
170
+ //# sourceMappingURL=mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutation.js","sourceRoot":"","sources":["../../../../src/core/lifecycle/mutation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface RecordOptions {
2
+ timestamps?: boolean;
3
+ softDelete?: boolean;
4
+ auth?: boolean;
5
+ }
6
+ export interface Actor {
7
+ id: string;
8
+ isAdmin?: boolean;
9
+ }
10
+ export type Authenticate = (authorization: unknown) => Actor;
11
+ export declare const DELETION_META = "djsDeletion";
12
+ export declare const AUDIT_FIELDS: readonly ['createdAt', 'updatedAt', 'deletedAt', 'createdById', 'updatedById', 'deletedById'];
13
+ /** Проверяет логические флаги и заменяет отсутствующие значения на false.
14
+ * @example recordOptions({ softDelete: true }) → { timestamps: false, softDelete: true, auth: false }.
15
+ */
16
+ export declare function recordOptions(options?: RecordOptions): Required<RecordOptions>;
17
+ /** Ищет условие deletedAt на текущем уровне и в логических ветках, не заходя в поля связанных объектов.
18
+ * @example mentionsDeletedAt({ or: [{ deletedAt: { ne: null } }] }) → true; { user: { deletedAt: {} } } → false.
19
+ */
20
+ export declare function mentionsDeletedAt(where: unknown): boolean;
@@ -0,0 +1,21 @@
1
+ import { isObject } from '../utils.js';
2
+ export const DELETION_META = 'djsDeletion';
3
+ export const AUDIT_FIELDS = ['createdAt', 'updatedAt', 'deletedAt', 'createdById', 'updatedById', 'deletedById'];
4
+ /** Проверяет логические флаги и заменяет отсутствующие значения на false.
5
+ * @example recordOptions({ softDelete: true }) → { timestamps: false, softDelete: true, auth: false }.
6
+ */
7
+ export function recordOptions(options = {}) {
8
+ for (const key of ['timestamps', 'softDelete', 'auth'])
9
+ if (options[key] !== undefined && typeof options[key] !== 'boolean')
10
+ throw new Error(`${key} must be boolean`);
11
+ return { timestamps: options.timestamps ?? false, softDelete: options.softDelete ?? false, auth: options.auth ?? false };
12
+ }
13
+ /** Ищет условие deletedAt на текущем уровне и в логических ветках, не заходя в поля связанных объектов.
14
+ * @example mentionsDeletedAt({ or: [{ deletedAt: { ne: null } }] }) → true; { user: { deletedAt: {} } } → false.
15
+ */
16
+ export function mentionsDeletedAt(where) {
17
+ if (!isObject(where))
18
+ return false;
19
+ return Object.hasOwn(where, 'deletedAt') || ['and', 'or'].some((key) => Array.isArray(where[key]) && where[key].some(mentionsDeletedAt)) || mentionsDeletedAt(where.not);
20
+ }
21
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../../../../src/core/lifecycle/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAYvC,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,CAAU,CAAC;AAC1H;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,OAAO,GAAkB,EAAE;IACvD,KAAK,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,CAAU;QAAE,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,kBAAkB,CAAC,CAAC;IAChL,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;AAC3H,CAAC;AACD;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC3K,CAAC"}
@@ -0,0 +1,130 @@
1
+ import { type RecordOptions } from './lifecycle/options.js';
2
+ import type { DatabaseData, JsonValue } from './types.js';
3
+ export interface Field {
4
+ type: string;
5
+ description?: string;
6
+ example?: JsonValue;
7
+ required?: boolean;
8
+ nullable?: boolean;
9
+ default?: JsonValue;
10
+ enum?: JsonValue[];
11
+ primary?: boolean;
12
+ generated?: 'uuid' | 'increment';
13
+ readOnly?: boolean;
14
+ writeOnly?: boolean;
15
+ minLength?: number;
16
+ maxLength?: number;
17
+ pattern?: string;
18
+ format?: 'date' | 'date-time' | 'email' | 'uri' | 'uuid';
19
+ minimum?: number;
20
+ maximum?: number;
21
+ source?: string;
22
+ target?: string;
23
+ onDelete?: 'restrict' | 'cascade';
24
+ }
25
+ export interface EntityDefinition {
26
+ timestamps?: boolean;
27
+ softDelete?: boolean;
28
+ collection: string;
29
+ api?: ('openapi' | 'graphql')[];
30
+ fields: Record<string, Field>;
31
+ }
32
+ export type ModelSchema = Record<string, EntityDefinition>;
33
+ export interface Node extends Field {
34
+ system?: boolean;
35
+ internal?: boolean;
36
+ softDelete?: boolean;
37
+ path: string;
38
+ base: string;
39
+ many: boolean;
40
+ children: Record<string, Node>;
41
+ relation?: Entity;
42
+ implicit?: boolean;
43
+ mixed?: boolean;
44
+ relationKey?: boolean;
45
+ }
46
+ export interface Entity {
47
+ timestamps: boolean;
48
+ softDelete: boolean;
49
+ name: string;
50
+ collection: string;
51
+ api: ('openapi' | 'graphql')[];
52
+ primary: string;
53
+ fields: Record<string, Node>;
54
+ root: Node;
55
+ }
56
+ export interface Model {
57
+ options: Required<RecordOptions>;
58
+ entities: Entity[];
59
+ byName: Map<string, Entity>;
60
+ byCollection: Map<string, Entity>;
61
+ explicit: boolean;
62
+ }
63
+ export type ValidationSchema = Record<string, unknown>;
64
+ /** Разбивает путь по точкам и проверяет допустимость каждого имени.
65
+ * @example pathParts('profile.name') → ['profile', 'name']; pathParts('a..b') → ошибка.
66
+ */
67
+ export declare const pathParts: (path: string) => string[];
68
+ /** Находит самый длинный префикс пути среди ключей объекта, учитывая границы сегментов.
69
+ * @example bindingFor({ a: {}, 'a.b': {} }, 'a.b.id') → 'a.b'; bindingFor({ a: {} }, 'abc.id') → undefined.
70
+ */
71
+ export declare function bindingFor(bindings: Record<string, unknown>, path: string): string | undefined;
72
+ /** Проверяет, что все сегменты пути объявлены и доступны для записи, включая родительские объекты.
73
+ * @example Для поля name без ограничений canWriteKey(entity, 'name') → true; для первичного ключа → false.
74
+ */
75
+ export declare function canWriteKey(entity: Entity, path: string): boolean;
76
+ /** Распознаёт обратную связь: источник совпадает с первичным ключом, а целевой путь отличается от первичного ключа цели.
77
+ * @example При source = 'id', target = 'authorId' и первичных ключах id → true.
78
+ */
79
+ export declare function isReverseRelation(entity: Entity, node: Node): boolean;
80
+ /** Собирает значения по вложенному пути, проходя через массивы; отсутствующие значения и null пропускает.
81
+ * @example readPath({ rows: [{ id: 1 }, { id: 2 }] }, 'rows.id') → [1, 2].
82
+ */
83
+ export declare const readPath: (value: unknown, path: string | string[]) => unknown[];
84
+ /** Возвращает последний сегмент пути поля.
85
+ * @example childName({ path: 'profile.name', … }) → 'name'.
86
+ */
87
+ export declare const childName: (node: Node) => string;
88
+ /** Для связи возвращает целевую сущность и её корень; обычный узел сохраняет без изменений.
89
+ * @example canonicalNode(author, { relation: book }) → [book, book.root].
90
+ * @example canonicalNode(author, author.root) → [author, author.root].
91
+ */
92
+ export declare const canonicalNode: (entity: Entity, node: Node) => [Entity, Node];
93
+ /** Соединяет имя сущности и путь поля, заменяя точки подчёркиваниями.
94
+ * @example nodeName({ name: 'User', … }, { path: 'profile.name', … }) → 'User_profile_name'.
95
+ */
96
+ export declare const nodeName: (entity: Entity, node: Node) => string;
97
+ /** Переводит первую букву имени сущности в нижний регистр.
98
+ * @example operationName({ name: 'UserProfile', … }) → 'userProfile'.
99
+ */
100
+ export declare const operationName: (entity: Entity) => string;
101
+ /** Загружает описание из объекта или JSON-файла, проверяет поля и сопоставляет связи.
102
+ * @example loadModel(undefined) → Promise<undefined>; корректное описание → Promise<Model>.
103
+ */
104
+ export declare function loadModel(source: unknown, settings?: RecordOptions): Promise<Model | undefined>;
105
+ /** Определяет доступность поля для выбранного режима записи, учитывая защищённые дочерние поля.
106
+ * @example writable({ readOnly: true, … }, 'update') → false.
107
+ */
108
+ export declare function writable(node: Node, mode: InputMode, relations?: boolean): boolean;
109
+ /** Проверяет наличие явной модели и доступность выбранного формата для её связей.
110
+ * @example assertApi(undefined, 'graphql') → ошибка; согласованная модель → undefined.
111
+ */
112
+ export declare function assertApi(model: Model | undefined, api: 'graphql' | 'openapi'): asserts model is Model;
113
+ /** Строит схему проверки значения с ограничениями, массивом и допустимостью null.
114
+ * @example Для строкового узла без ограничений valueSchema(node) → { type: 'string' }.
115
+ */
116
+ export declare function valueSchema(node: Node): ValidationSchema;
117
+ export type InputMode = 'stored' | 'create' | 'replace' | 'update';
118
+ /** Строит схему объекта для выбранного режима; при частичном обновлении корневые поля необязательны.
119
+ * @example Для пустого узла → { type: 'object', properties: {}, additionalProperties: false }.
120
+ */
121
+ export declare function objectSchema(node: Node, mode: InputMode, root?: boolean, relationSchema?: (node: Node) => ValidationSchema): ValidationSchema;
122
+ export declare function relationInputSchema(node: Node, object?: ValidationSchema): ValidationSchema;
123
+ /** Проверяет запись по схеме выбранного режима и выбрасывает исключение при несоответствии.
124
+ * @example Обязательное строковое name: { name: 'Анна' } → undefined; { name: 2 } → ошибка.
125
+ */
126
+ export declare function validateRecord(entity: Entity, value: unknown, mode: InputMode, relations?: boolean): void;
127
+ /** Выводит поля и связи из значений коллекций, не меняя переданные записи.
128
+ * @example inferModel({ users: [{ id: '1', name: 'Анна' }] }) → модель со строковыми id и name.
129
+ */
130
+ export declare function inferModel(database: DatabaseData, settings?: RecordOptions): Model;
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { Ajv } from 'ajv';
3
3
  import addFormats from 'ajv-formats';
4
4
  import { domainError } from './errors.js';
5
+ import { AUDIT_FIELDS, DELETION_META, recordOptions } from './lifecycle/options.js';
5
6
  import { getRelationMetadata } from './relation-metadata.js';
6
7
  import { assertKnownKeys, isObject, isSafeKey, singularize, toPascalCase } from './utils.js';
7
8
  const createValidator = () => {
@@ -34,13 +35,18 @@ const FIELD_KEYS = new Set([
34
35
  'target',
35
36
  'onDelete',
36
37
  ]);
38
+ /** Разбивает путь по точкам и проверяет допустимость каждого имени.
39
+ * @example pathParts('profile.name') → ['profile', 'name']; pathParts('a..b') → ошибка.
40
+ */
37
41
  export const pathParts = (path) => {
38
42
  const parts = path.split('.');
39
43
  if (parts.some((p) => !NAME.test(p) || !isSafeKey(p)))
40
44
  throw domainError('INVALID_INPUT', `Invalid field path: ${path}`);
41
45
  return parts;
42
46
  };
43
- /** Finds the nearest physical object containing a relation key. */
47
+ /** Находит самый длинный префикс пути среди ключей объекта, учитывая границы сегментов.
48
+ * @example bindingFor({ a: {}, 'a.b': {} }, 'a.b.id') → 'a.b'; bindingFor({ a: {} }, 'abc.id') → undefined.
49
+ */
44
50
  export function bindingFor(bindings, path) {
45
51
  let nearest;
46
52
  for (const prefix of Object.keys(bindings))
@@ -48,6 +54,9 @@ export function bindingFor(bindings, path) {
48
54
  nearest = prefix;
49
55
  return nearest;
50
56
  }
57
+ /** Проверяет, что все сегменты пути объявлены и доступны для записи, включая родительские объекты.
58
+ * @example Для поля name без ограничений canWriteKey(entity, 'name') → true; для первичного ключа → false.
59
+ */
51
60
  export function canWriteKey(entity, path) {
52
61
  const parts = pathParts(path);
53
62
  return parts.every((_, index) => {
@@ -55,9 +64,15 @@ export function canWriteKey(entity, path) {
55
64
  return field && !field.primary && !field.generated && !field.readOnly;
56
65
  });
57
66
  }
67
+ /** Распознаёт обратную связь: источник совпадает с первичным ключом, а целевой путь отличается от первичного ключа цели.
68
+ * @example При source = 'id', target = 'authorId' и первичных ключах id → true.
69
+ */
58
70
  export function isReverseRelation(entity, node) {
59
71
  return node.source === entity.primary || (!canWriteKey(entity, node.source) && canWriteKey(node.relation, node.target));
60
72
  }
73
+ /** Собирает значения по вложенному пути, проходя через массивы; отсутствующие значения и null пропускает.
74
+ * @example readPath({ rows: [{ id: 1 }, { id: 2 }] }, 'rows.id') → [1, 2].
75
+ */
61
76
  export const readPath = (value, path) => {
62
77
  const parts = typeof path === 'string' ? pathParts(path) : path;
63
78
  if (!parts.length)
@@ -66,8 +81,22 @@ export const readPath = (value, path) => {
66
81
  return value.flatMap((v) => readPath(v, parts));
67
82
  return isObject(value) && Object.hasOwn(value, parts[0]) ? readPath(value[parts[0]], parts.slice(1)) : [];
68
83
  };
84
+ /** Возвращает последний сегмент пути поля.
85
+ * @example childName({ path: 'profile.name', … }) → 'name'.
86
+ */
69
87
  export const childName = (node) => node.path.split('.').at(-1);
88
+ /** Для связи возвращает целевую сущность и её корень; обычный узел сохраняет без изменений.
89
+ * @example canonicalNode(author, { relation: book }) → [book, book.root].
90
+ * @example canonicalNode(author, author.root) → [author, author.root].
91
+ */
92
+ export const canonicalNode = (entity, node) => (node.relation ? [node.relation, node.relation.root] : [entity, node]);
93
+ /** Соединяет имя сущности и путь поля, заменяя точки подчёркиваниями.
94
+ * @example nodeName({ name: 'User', … }, { path: 'profile.name', … }) → 'User_profile_name'.
95
+ */
70
96
  export const nodeName = (entity, node) => (node.path ? `${entity.name}_${node.path.replaceAll('.', '_')}` : entity.name);
97
+ /** Переводит первую букву имени сущности в нижний регистр.
98
+ * @example operationName({ name: 'UserProfile', … }) → 'userProfile'.
99
+ */
71
100
  export const operationName = (entity) => entity.name[0].toLowerCase() + entity.name.slice(1);
72
101
  const newNode = (path, field) => ({ ...field, path, base: field.type.replace(/\[\]$/, ''), many: field.type.endsWith('[]'), children: Object.create(null) });
73
102
  function addField(entity, path, field, implicit = false) {
@@ -91,6 +120,25 @@ function addField(entity, path, field, implicit = false) {
91
120
  entity.fields[path] = node;
92
121
  return node;
93
122
  }
123
+ function systemFields(entity, options) {
124
+ const names = [
125
+ ...(entity.timestamps ? ['createdAt', 'updatedAt'] : []),
126
+ DELETION_META,
127
+ ...(entity.softDelete ? ['deletedAt'] : []),
128
+ ...(options.auth ? ['createdById', 'updatedById', ...(entity.softDelete ? ['deletedById'] : [])] : []),
129
+ ];
130
+ entity.root.softDelete = entity.softDelete;
131
+ for (const name of names) {
132
+ if (entity.fields[name])
133
+ throw new Error(`Reserved system field ${entity.name}.${name}`);
134
+ const node = addField(entity, name, { type: 'string', readOnly: true, nullable: true, ...(name.endsWith('At') ? { format: 'date-time' } : {}) });
135
+ node.system = true;
136
+ if (name === DELETION_META) {
137
+ node.internal = true;
138
+ node.writeOnly = true;
139
+ }
140
+ }
141
+ }
94
142
  function checkField(path, field) {
95
143
  if (!isObject(field))
96
144
  throw new Error(`Field ${path} must be an object`);
@@ -139,7 +187,11 @@ function checkField(path, field) {
139
187
  if (typeof field.minLength === 'number' && typeof field.maxLength === 'number' && field.minLength > field.maxLength)
140
188
  throw new Error(`${path}: minLength exceeds maxLength`);
141
189
  }
142
- export async function loadModel(source) {
190
+ /** Загружает описание из объекта или JSON-файла, проверяет поля и сопоставляет связи.
191
+ * @example loadModel(undefined) → Promise<undefined>; корректное описание → Promise<Model>.
192
+ */
193
+ export async function loadModel(source, settings = {}) {
194
+ const options = recordOptions(settings);
143
195
  if (source === undefined)
144
196
  return undefined;
145
197
  const schema = typeof source === 'string' ? JSON.parse(await readFile(source, 'utf8')) : structuredClone(source);
@@ -148,11 +200,11 @@ export async function loadModel(source) {
148
200
  throw new Error('Model schema must be a nonempty object');
149
201
  if ('$schema' in schema || '$info' in schema)
150
202
  throw new Error('Legacy $schema/$info format is no longer supported');
151
- const model = { entities: [], byName: new Map(), byCollection: new Map(), explicit: true };
203
+ const model = { entities: [], byName: new Map(), byCollection: new Map(), explicit: true, options };
152
204
  for (const [name, definition] of Object.entries(schema)) {
153
205
  if (!NAME.test(name) || !isSafeKey(name) || PRIMITIVES.has(name) || !isObject(definition))
154
206
  throw new Error(`Invalid model ${name}`);
155
- assertKnownKeys(definition, new Set(['collection', 'api', 'fields']), name);
207
+ assertKnownKeys(definition, new Set(['collection', 'api', 'fields', 'timestamps', 'softDelete']), name);
156
208
  if (typeof definition.collection !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(definition.collection) || !isSafeKey(definition.collection))
157
209
  throw new Error(`Invalid collection for ${name}`);
158
210
  if (model.byCollection.has(definition.collection))
@@ -162,7 +214,17 @@ export async function loadModel(source) {
162
214
  throw new Error(`Invalid api for ${name}`);
163
215
  if (!isObject(definition.fields))
164
216
  throw new Error(`${name}.fields must be an object`);
165
- const entity = { name, collection: definition.collection, api, primary: '', fields: Object.create(null), root: newNode('', { type: 'object' }) };
217
+ const flags = recordOptions({ timestamps: definition.timestamps, softDelete: definition.softDelete });
218
+ const entity = {
219
+ timestamps: definition.timestamps === undefined ? options.timestamps : flags.timestamps,
220
+ softDelete: definition.softDelete === undefined ? options.softDelete : flags.softDelete,
221
+ name,
222
+ collection: definition.collection,
223
+ api,
224
+ primary: '',
225
+ fields: Object.create(null),
226
+ root: newNode('', { type: 'object' }),
227
+ };
166
228
  for (const [path, field] of Object.entries(definition.fields).sort(([a], [b]) => a.split('.').length - b.split('.').length)) {
167
229
  checkField(path, field);
168
230
  addField(entity, path, field);
@@ -175,6 +237,7 @@ export async function loadModel(source) {
175
237
  }
176
238
  if (!entity.primary)
177
239
  throw new Error(`${name}: primary key is required`);
240
+ systemFields(entity, options);
178
241
  entityValidators.set(entity, ajv);
179
242
  model.entities.push(entity);
180
243
  model.byName.set(name, entity);
@@ -254,6 +317,9 @@ export async function loadModel(source) {
254
317
  }
255
318
  return model;
256
319
  }
320
+ /** Определяет доступность поля для выбранного режима записи, учитывая защищённые дочерние поля.
321
+ * @example writable({ readOnly: true, … }, 'update') → false.
322
+ */
257
323
  export function writable(node, mode, relations = false) {
258
324
  if (node.relation)
259
325
  return relations;
@@ -262,6 +328,9 @@ export function writable(node, mode, relations = false) {
262
328
  const children = Object.values(node.children);
263
329
  return node.base !== 'object' || children.length === 0 || children.some((child) => writable(child, mode, relations));
264
330
  }
331
+ /** Проверяет наличие явной модели и доступность выбранного формата для её связей.
332
+ * @example assertApi(undefined, 'graphql') → ошибка; согласованная модель → undefined.
333
+ */
265
334
  export function assertApi(model, api) {
266
335
  if (!model?.explicit)
267
336
  throw new Error(`${api} requires an explicit model schema`);
@@ -270,6 +339,9 @@ export function assertApi(model, api) {
270
339
  if (node.relation && !node.relation.api.includes(api))
271
340
  throw new Error(`${entity.name}.${node.path}: ${node.relation.name} does not enable ${api}`);
272
341
  }
342
+ /** Строит схему проверки значения с ограничениями, массивом и допустимостью null.
343
+ * @example Для строкового узла без ограничений valueSchema(node) → { type: 'string' }.
344
+ */
273
345
  export function valueSchema(node) {
274
346
  let schema = node.base === 'object' ? objectSchema(node, 'stored') : { type: node.base };
275
347
  for (const key of ['minLength', 'maxLength', 'pattern', 'format', 'minimum', 'maximum', 'enum'])
@@ -281,6 +353,9 @@ export function valueSchema(node) {
281
353
  schema = { anyOf: [schema, { type: 'null' }] };
282
354
  return schema;
283
355
  }
356
+ /** Строит схему объекта для выбранного режима; при частичном обновлении корневые поля необязательны.
357
+ * @example Для пустого узла → { type: 'object', properties: {}, additionalProperties: false }.
358
+ */
284
359
  export function objectSchema(node, mode, root = false, relationSchema) {
285
360
  const properties = {};
286
361
  const required = [];
@@ -304,6 +379,10 @@ export function objectSchema(node, mode, root = false, relationSchema) {
304
379
  if (child.required && !(root && mode === 'update') && !(mode !== 'stored' && child.default !== undefined) && !(relationSchema && child.relationKey))
305
380
  required.push(key);
306
381
  }
382
+ // Сохраняем ранее записанные даты и авторов при отключении соответствующих флагов.
383
+ if (root && mode === 'stored')
384
+ for (const name of AUDIT_FIELDS)
385
+ properties[name] ??= { type: ['string', 'null'], ...(name.endsWith('At') ? { format: 'date-time' } : {}) };
307
386
  return { type: 'object', properties, additionalProperties: false, ...(required.length ? { required } : {}) };
308
387
  }
309
388
  export function relationInputSchema(node, object = { type: 'object' }) {
@@ -316,6 +395,9 @@ export function relationInputSchema(node, object = { type: 'object' }) {
316
395
  return schema;
317
396
  }
318
397
  const validators = new WeakMap();
398
+ /** Проверяет запись по схеме выбранного режима и выбрасывает исключение при несоответствии.
399
+ * @example Обязательное строковое name: { name: 'Анна' } → undefined; { name: 2 } → ошибка.
400
+ */
319
401
  export function validateRecord(entity, value, mode, relations = false) {
320
402
  const ajv = entityValidators.get(entity);
321
403
  let cache = validators.get(entity);
@@ -332,16 +414,30 @@ export function validateRecord(entity, value, mode, relations = false) {
332
414
  if (!validate(value))
333
415
  throw domainError('INVALID_INPUT', `${entity.name}: ${ajv.errorsText(validate.errors)}`);
334
416
  }
335
- export function inferModel(database) {
417
+ /** Выводит поля и связи из значений коллекций, не меняя переданные записи.
418
+ * @example inferModel({ users: [{ id: '1', name: 'Анна' }] }) → модель со строковыми id и name.
419
+ */
420
+ export function inferModel(database, settings = {}) {
421
+ const options = recordOptions(settings);
336
422
  const valueType = (value) => (isObject(value) ? 'object' : ['string', 'number', 'boolean'].includes(typeof value) ? typeof value : 'string');
337
- const model = { entities: [], byName: new Map(), byCollection: new Map(), explicit: false };
423
+ const model = { entities: [], byName: new Map(), byCollection: new Map(), explicit: false, options };
338
424
  for (const [collection, records] of Object.entries(database)) {
339
425
  const name = toPascalCase(singularize(collection));
340
- const entity = { name, collection, api: [], primary: 'id', fields: Object.create(null), root: newNode('', { type: 'object' }) };
426
+ const entity = {
427
+ timestamps: options.timestamps,
428
+ softDelete: options.softDelete,
429
+ name,
430
+ collection,
431
+ api: [],
432
+ primary: 'id',
433
+ fields: Object.create(null),
434
+ root: newNode('', { type: 'object' }),
435
+ };
436
+ systemFields(entity, options);
341
437
  const observed = new Map();
342
438
  const scan = (record, prefix = '') => {
343
439
  for (const [key, value] of Object.entries(record)) {
344
- if (!NAME.test(key) || !isSafeKey(key))
440
+ if (!NAME.test(key) || !isSafeKey(key) || (!prefix && entity.fields[key]?.system))
345
441
  continue;
346
442
  const path = prefix + key;
347
443
  const sample = Array.isArray(value) ? value.find((v) => v !== null) : value;
@@ -382,7 +478,7 @@ export function inferModel(database) {
382
478
  const resources = Object.keys(database);
383
479
  for (const entity of model.entities)
384
480
  for (const field of Object.values(entity.fields)) {
385
- if (field.mixed)
481
+ if (field.mixed || field.system)
386
482
  continue;
387
483
  const relation = getRelationMetadata(childName(field), resources, entity.collection);
388
484
  if (!relation)