@kollors/deep-json-server 1.0.0-alpha.6 → 1.0.0-alpha.8

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 (237) hide show
  1. package/README.md +373 -230
  2. package/README.ru.md +370 -227
  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 +10 -8
  6. package/dist/index.js +3 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +6 -2
  9. package/dist/src/auth/contract.js +8 -1
  10. package/dist/src/auth/contract.js.map +1 -1
  11. package/dist/src/auth/input.d.ts +18 -0
  12. package/dist/src/auth/input.js +34 -0
  13. package/dist/src/auth/input.js.map +1 -0
  14. package/dist/src/auth/password.d.ts +13 -0
  15. package/dist/src/auth/password.js +17 -1
  16. package/dist/src/auth/password.js.map +1 -1
  17. package/dist/src/auth/routes.d.ts +3 -0
  18. package/dist/src/auth/routes.js +6 -0
  19. package/dist/src/auth/routes.js.map +1 -1
  20. package/dist/src/auth/service.d.ts +64 -4
  21. package/dist/src/auth/service.js +167 -61
  22. package/dist/src/auth/service.js.map +1 -1
  23. package/dist/src/auth/store.d.ts +26 -0
  24. package/dist/src/auth/store.js +82 -0
  25. package/dist/src/auth/store.js.map +1 -0
  26. package/dist/src/cli/index.d.ts +7 -0
  27. package/dist/src/{cli.js → cli/index.js} +31 -15
  28. package/dist/src/cli/index.js.map +1 -0
  29. package/dist/src/core/config-values.d.ts +28 -0
  30. package/dist/src/core/config-values.js +53 -0
  31. package/dist/src/core/config-values.js.map +1 -0
  32. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -1
  33. package/dist/src/{constants.js → core/constants.js} +1 -1
  34. package/dist/src/core/constants.js.map +1 -0
  35. package/dist/src/core/database.d.ts +53 -0
  36. package/dist/src/{database.js → core/database.js} +23 -3
  37. package/dist/src/core/database.js.map +1 -0
  38. package/dist/src/core/engine.d.ts +65 -0
  39. package/dist/src/{engine.js → core/engine.js} +114 -59
  40. package/dist/src/core/engine.js.map +1 -0
  41. package/dist/src/{errors.d.ts → core/errors.d.ts} +4 -1
  42. package/dist/src/core/errors.js +12 -0
  43. package/dist/src/core/errors.js.map +1 -0
  44. package/dist/src/core/http-errors.d.ts +9 -0
  45. package/dist/src/core/http-errors.js +15 -0
  46. package/dist/src/core/http-errors.js.map +1 -0
  47. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  48. package/dist/src/core/lifecycle/mutation.js +170 -0
  49. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  50. package/dist/src/core/lifecycle/options.d.ts +21 -0
  51. package/dist/src/core/lifecycle/options.js +19 -0
  52. package/dist/src/core/lifecycle/options.js.map +1 -0
  53. package/dist/src/core/model.d.ts +131 -0
  54. package/dist/src/{model.js → core/model.js} +106 -10
  55. package/dist/src/core/model.js.map +1 -0
  56. package/dist/src/{mutations → core/mutations}/write.d.ts +4 -4
  57. package/dist/src/{mutations → core/mutations}/write.js +14 -4
  58. package/dist/src/core/mutations/write.js.map +1 -0
  59. package/dist/src/core/operations.d.ts +33 -0
  60. package/dist/src/core/operations.js +10 -0
  61. package/dist/src/core/operations.js.map +1 -0
  62. package/dist/src/core/pagination.d.ts +10 -0
  63. package/dist/src/core/pagination.js +13 -0
  64. package/dist/src/core/pagination.js.map +1 -0
  65. package/dist/src/core/paths.d.ts +16 -0
  66. package/dist/src/{paths.js → core/paths.js} +16 -0
  67. package/dist/src/core/paths.js.map +1 -0
  68. package/dist/src/core/query/contract.d.ts +6 -0
  69. package/dist/src/{query → core/query}/contract.js +3 -0
  70. package/dist/src/core/query/contract.js.map +1 -0
  71. package/dist/src/core/query/filter.d.ts +6 -0
  72. package/dist/src/{query → core/query}/filter.js +18 -4
  73. package/dist/src/core/query/filter.js.map +1 -0
  74. package/dist/src/core/query/options.d.ts +30 -0
  75. package/dist/src/{query → core/query}/options.js +12 -0
  76. package/dist/src/core/query/options.js.map +1 -0
  77. package/dist/src/core/records.d.ts +46 -0
  78. package/dist/src/{records.js → core/records.js} +24 -3
  79. package/dist/src/core/records.js.map +1 -0
  80. package/dist/src/core/relation-metadata.d.ts +15 -0
  81. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -1
  82. package/dist/src/core/relation-metadata.js.map +1 -0
  83. package/dist/src/core/types.d.ts +10 -0
  84. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  85. package/dist/src/core/utils.d.ts +56 -0
  86. package/dist/src/core/utils.js +102 -0
  87. package/dist/src/core/utils.js.map +1 -0
  88. package/dist/src/files/contract.d.ts +33 -0
  89. package/dist/src/files/contract.js +25 -3
  90. package/dist/src/files/contract.js.map +1 -1
  91. package/dist/src/files/disk-store.d.ts +3 -0
  92. package/dist/src/files/disk-store.js +25 -18
  93. package/dist/src/files/disk-store.js.map +1 -1
  94. package/dist/src/files/http.d.ts +8 -3
  95. package/dist/src/files/http.js +9 -0
  96. package/dist/src/files/http.js.map +1 -1
  97. package/dist/src/files/index.d.ts +4 -3
  98. package/dist/src/files/index.js +3 -1
  99. package/dist/src/files/index.js.map +1 -1
  100. package/dist/src/files/memory-store.d.ts +4 -1
  101. package/dist/src/files/memory-store.js +21 -15
  102. package/dist/src/files/memory-store.js.map +1 -1
  103. package/dist/src/files/routes.d.ts +3 -0
  104. package/dist/src/files/routes.js +25 -1
  105. package/dist/src/files/routes.js.map +1 -1
  106. package/dist/src/files/streams.d.ts +5 -0
  107. package/dist/src/files/streams.js +20 -0
  108. package/dist/src/files/streams.js.map +1 -0
  109. package/dist/src/graphql/entry.d.ts +12 -3
  110. package/dist/src/graphql/entry.js +12 -2
  111. package/dist/src/graphql/entry.js.map +1 -1
  112. package/dist/src/graphql/generate.d.ts +11 -0
  113. package/dist/src/graphql/generate.js +17 -0
  114. package/dist/src/graphql/generate.js.map +1 -0
  115. package/dist/src/graphql/preflight.d.ts +4 -1
  116. package/dist/src/graphql/preflight.js +3 -0
  117. package/dist/src/graphql/preflight.js.map +1 -1
  118. package/dist/src/graphql/resolvers.d.ts +10 -2
  119. package/dist/src/graphql/resolvers.js +8 -2
  120. package/dist/src/graphql/resolvers.js.map +1 -1
  121. package/dist/src/graphql/routes.d.ts +6 -2
  122. package/dist/src/graphql/routes.js +6 -3
  123. package/dist/src/graphql/routes.js.map +1 -1
  124. package/dist/src/graphql/schema.d.ts +6 -0
  125. package/dist/src/{graphql.js → graphql/schema.js} +20 -14
  126. package/dist/src/graphql/schema.js.map +1 -0
  127. package/dist/src/graphql/write.d.ts +3 -0
  128. package/dist/src/graphql/write.js +3 -0
  129. package/dist/src/graphql/write.js.map +1 -1
  130. package/dist/src/openapi/auth.d.ts +13 -0
  131. package/dist/src/openapi/auth.js +139 -0
  132. package/dist/src/openapi/auth.js.map +1 -0
  133. package/dist/src/openapi/document.d.ts +5 -2
  134. package/dist/src/openapi/document.js +41 -42
  135. package/dist/src/openapi/document.js.map +1 -1
  136. package/dist/src/openapi/entry.d.ts +14 -3
  137. package/dist/src/openapi/entry.js +12 -1
  138. package/dist/src/openapi/entry.js.map +1 -1
  139. package/dist/src/openapi/files.d.ts +6 -0
  140. package/dist/src/{files/openapi.js → openapi/files.js} +6 -3
  141. package/dist/src/openapi/files.js.map +1 -0
  142. package/dist/src/openapi/generate.d.ts +19 -0
  143. package/dist/src/openapi/generate.js +39 -0
  144. package/dist/src/openapi/generate.js.map +1 -0
  145. package/dist/src/openapi/helpers.d.ts +10 -1
  146. package/dist/src/openapi/helpers.js +9 -0
  147. package/dist/src/openapi/helpers.js.map +1 -1
  148. package/dist/src/openapi/options.d.ts +20 -0
  149. package/dist/src/openapi/options.js +13 -0
  150. package/dist/src/openapi/options.js.map +1 -0
  151. package/dist/src/{types.d.ts → openapi/types.d.ts} +1 -10
  152. package/dist/src/openapi/types.js +2 -0
  153. package/dist/src/openapi/types.js.map +1 -0
  154. package/dist/src/openapi/write.d.ts +5 -0
  155. package/dist/src/openapi/write.js +12 -0
  156. package/dist/src/openapi/write.js.map +1 -0
  157. package/dist/src/rest/options.d.ts +11 -2
  158. package/dist/src/rest/options.js +12 -3
  159. package/dist/src/rest/options.js.map +1 -1
  160. package/dist/src/rest/projection.d.ts +10 -4
  161. package/dist/src/rest/projection.js +10 -4
  162. package/dist/src/rest/projection.js.map +1 -1
  163. package/dist/src/rest/routes.d.ts +6 -2
  164. package/dist/src/rest/routes.js +12 -11
  165. package/dist/src/rest/routes.js.map +1 -1
  166. package/dist/src/server/config.d.ts +76 -0
  167. package/dist/src/{config.js → server/config.js} +71 -74
  168. package/dist/src/server/config.js.map +1 -0
  169. package/dist/src/server/create.d.ts +18 -0
  170. package/dist/src/{server.js → server/create.js} +33 -28
  171. package/dist/src/server/create.js.map +1 -0
  172. package/dist/src/server/features.d.ts +15 -0
  173. package/dist/src/{features.js → server/features.js} +8 -2
  174. package/dist/src/server/features.js.map +1 -0
  175. package/dist/src/server/public.d.ts +3 -3
  176. package/dist/src/server/public.js +1 -1
  177. package/dist/src/server/public.js.map +1 -1
  178. package/package.json +1 -1
  179. package/dist/src/auth/openapi.d.ts +0 -10
  180. package/dist/src/auth/openapi.js +0 -62
  181. package/dist/src/auth/openapi.js.map +0 -1
  182. package/dist/src/cli.d.ts +0 -4
  183. package/dist/src/cli.js.map +0 -1
  184. package/dist/src/config.d.ts +0 -86
  185. package/dist/src/config.js.map +0 -1
  186. package/dist/src/constants.js.map +0 -1
  187. package/dist/src/database.d.ts +0 -20
  188. package/dist/src/database.js.map +0 -1
  189. package/dist/src/engine.d.ts +0 -36
  190. package/dist/src/engine.js.map +0 -1
  191. package/dist/src/errors.js +0 -9
  192. package/dist/src/errors.js.map +0 -1
  193. package/dist/src/features.d.ts +0 -9
  194. package/dist/src/features.js.map +0 -1
  195. package/dist/src/files/openapi.d.ts +0 -3
  196. package/dist/src/files/openapi.js.map +0 -1
  197. package/dist/src/graphql/public.d.ts +0 -3
  198. package/dist/src/graphql/public.js +0 -11
  199. package/dist/src/graphql/public.js.map +0 -1
  200. package/dist/src/graphql.d.ts +0 -3
  201. package/dist/src/graphql.js.map +0 -1
  202. package/dist/src/http/errors.d.ts +0 -6
  203. package/dist/src/http/errors.js +0 -3
  204. package/dist/src/http/errors.js.map +0 -1
  205. package/dist/src/model.d.ts +0 -72
  206. package/dist/src/model.js.map +0 -1
  207. package/dist/src/mutations/write.js.map +0 -1
  208. package/dist/src/openapi/index.d.ts +0 -7
  209. package/dist/src/openapi/index.js +0 -28
  210. package/dist/src/openapi/index.js.map +0 -1
  211. package/dist/src/openapi/public.d.ts +0 -18
  212. package/dist/src/openapi/public.js +0 -20
  213. package/dist/src/openapi/public.js.map +0 -1
  214. package/dist/src/pagination.d.ts +0 -7
  215. package/dist/src/pagination.js +0 -12
  216. package/dist/src/pagination.js.map +0 -1
  217. package/dist/src/paths.d.ts +0 -7
  218. package/dist/src/paths.js.map +0 -1
  219. package/dist/src/query/contract.d.ts +0 -3
  220. package/dist/src/query/contract.js.map +0 -1
  221. package/dist/src/query/filter.d.ts +0 -3
  222. package/dist/src/query/filter.js.map +0 -1
  223. package/dist/src/query/options.d.ts +0 -18
  224. package/dist/src/query/options.js.map +0 -1
  225. package/dist/src/records.d.ts +0 -25
  226. package/dist/src/records.js.map +0 -1
  227. package/dist/src/relation-metadata.d.ts +0 -9
  228. package/dist/src/relation-metadata.js.map +0 -1
  229. package/dist/src/schema.d.ts +0 -8
  230. package/dist/src/schema.js +0 -13
  231. package/dist/src/schema.js.map +0 -1
  232. package/dist/src/server.d.ts +0 -12
  233. package/dist/src/server.js.map +0 -1
  234. package/dist/src/utils.d.ts +0 -12
  235. package/dist/src/utils.js +0 -58
  236. package/dist/src/utils.js.map +0 -1
  237. /package/dist/src/{types.js → core/types.js} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { DEFAULT_MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE } from './constants.js';
2
2
  import { domainError } from './errors.js';
3
+ import { RecordMutation } from './lifecycle/mutation.js';
3
4
  import { inferModel, isReverseRelation, pathParts, readPath, validateRecord } from './model.js';
4
5
  import { MutationWriter } from './mutations/write.js';
5
6
  import { compileWhere } from './query/filter.js';
6
7
  import { badQuery, childrenOf, nodeAt } from './query/options.js';
7
8
  import { isRef, keyOf, makeContext, related, resolveField, rootRef, sourceValues } from './records.js';
8
- import { isObject } from './utils.js';
9
+ import { hasOnlyKeys, isObject } from './utils.js';
9
10
  function filterView(ref) {
10
11
  const value = {};
11
12
  for (const [name, node] of Object.entries(childrenOf(ref.node)))
@@ -13,7 +14,7 @@ function filterView(ref) {
13
14
  Object.defineProperty(value, name, {
14
15
  enumerable: true,
15
16
  get: () => {
16
- const field = resolveField(ref, node);
17
+ const field = resolveField(ref, node, true);
17
18
  return isRef(field) ? filterView(field) : Array.isArray(field) ? field.map((v) => (isRef(v) ? filterView(v) : v)) : field;
18
19
  },
19
20
  });
@@ -33,41 +34,56 @@ export class Engine {
33
34
  this.maxPageSize = maxPageSize;
34
35
  this.inferredModels.set(store.database.data, model);
35
36
  }
37
+ /** Возвращает явную модель или кешированное описание, выведенное из переданного снимка данных.
38
+ * @example Повторный вызов с тем же объектом data → тот же объект Model.
39
+ */
36
40
  modelFor(data) {
37
41
  if (this.model.explicit)
38
42
  return this.model;
39
43
  let model = this.inferredModels.get(data);
40
44
  if (!model) {
41
- model = inferModel(data);
45
+ model = inferModel(data, this.model.options);
42
46
  this.inferredModels.set(data, model);
43
47
  }
44
48
  return model;
45
49
  }
50
+ /** Читает актуальный снимок хранилища и создаёт контекст с пустым кешем индексов.
51
+ * @example context() → Promise<Context>; при ошибке чтения хранилища → отклонённый Promise.
52
+ */
46
53
  async context() {
47
54
  const data = await this.store.read();
48
55
  if (!this.model.explicit)
49
56
  this.model = this.modelFor(data);
50
57
  return makeContext(data, this.model);
51
58
  }
59
+ /** Оборачивает все записи выбранной коллекции ссылками с контекстом.
60
+ * @example Пустая коллекция → []; две записи → две ссылки Ref.
61
+ */
52
62
  records(context, entity) {
53
63
  return (context.data[entity.collection] ?? []).map((value) => rootRef(context, entity, value));
54
64
  }
65
+ /** Ищет запись по строковому представлению первичного ключа, включая удалённые записи.
66
+ * @example Ключ '1' находит запись с числовым ключом 1; отсутствующий ключ → undefined.
67
+ */
55
68
  find(context, entity, key) {
56
69
  const value = (context.data[entity.collection] ?? []).find((record) => String(record[entity.primary]) === String(key));
57
70
  return value ? rootRef(context, entity, value) : undefined;
58
71
  }
72
+ /** Проверяет аргументы списка и подготавливает предикат, пути сортировки и размеры страницы.
73
+ * @example pager: { page: 2, pageSize: 5 } → план с page: 2 и pageSize: 5; page: 0 → ошибка.
74
+ */
59
75
  prepareOptions(node, options = {}) {
60
- const predicate = options.where !== undefined ? compileWhere(node, options.where) : undefined;
76
+ const predicate = options.where !== undefined || node.softDelete || node.relation?.softDelete ? compileWhere(node, options.where ?? {}) : undefined;
61
77
  if (options.order !== undefined) {
62
78
  if (!Array.isArray(options.order))
63
79
  badQuery('order must be an array');
64
80
  for (const rule of options.order) {
65
- if (!isObject(rule) || Object.keys(rule).some((k) => !['field', 'direction'].includes(k)) || typeof rule.field !== 'string' || !['ASC', 'DESC'].includes(rule.direction))
81
+ if (!isObject(rule) || !hasOnlyKeys(rule, ['field', 'direction']) || typeof rule.field !== 'string' || !['ASC', 'DESC'].includes(rule.direction))
66
82
  badQuery('Invalid order rule');
67
83
  nodeAt(node, rule.field, true);
68
84
  }
69
85
  }
70
- if (options.pager !== undefined && (!isObject(options.pager) || Object.keys(options.pager).some((k) => !['page', 'pageSize'].includes(k))))
86
+ if (options.pager !== undefined && (!isObject(options.pager) || !hasOnlyKeys(options.pager, ['page', 'pageSize'])))
71
87
  badQuery('Invalid pager');
72
88
  const page = options.pager?.page ?? 1;
73
89
  const pageSize = options.pager?.pageSize ?? this.pageSize;
@@ -82,30 +98,40 @@ export class Engine {
82
98
  badQuery(`Invalid pager; pageSize must be 1..${this.maxPageSize}`);
83
99
  return { page, pageSize, predicate, rules: (options.order ?? []).map((rule) => ({ direction: rule.direction, keys: pathParts(rule.field) })) };
84
100
  }
101
+ /** Фильтрует, сортирует и возвращает страницу, не меняя порядок исходного массива.
102
+ * @example Три подходящие записи, page: 2, pageSize: 2 → { data: [третья запись], total: 3 }.
103
+ */
85
104
  list(records, node, options = {}, prepared = this.prepareOptions(node, options)) {
86
105
  const { page, pageSize, predicate, rules } = prepared;
87
- const data = predicate ? records.filter((ref) => predicate(filterView(ref))) : [...records];
88
- if (rules.length)
89
- data.sort((a, b) => {
90
- for (const rule of rules) {
91
- const left = readPath(a.value, rule.keys)[0];
92
- const right = readPath(b.value, rule.keys)[0];
93
- const comparison = left == null && right == null
94
- ? 0
95
- : left == null
96
- ? 1
97
- : right == null
98
- ? -1
99
- : typeof left === 'number' && typeof right === 'number'
100
- ? left - right
101
- : collator.compare(String(left), String(right));
102
- if (comparison)
103
- return rule.direction === 'DESC' ? -comparison : comparison;
104
- }
105
- return 0;
106
- });
107
- return { data: data.slice((page - 1) * pageSize, page * pageSize), total: data.length };
106
+ const start = (page - 1) * pageSize;
107
+ const data = predicate ? records.filter((ref) => predicate(filterView(ref))) : records;
108
+ if (!rules.length)
109
+ return { data: data.slice(start, start + pageSize), total: data.length };
110
+ // Извлекаем ключи один раз на запись, а не при каждом сравнении сортировки.
111
+ const ordered = data.map((ref) => ({ ref, keys: rules.map((rule) => readPath(ref.value, rule.keys)[0]) }));
112
+ ordered.sort((a, b) => {
113
+ for (let index = 0; index < rules.length; index++) {
114
+ const left = a.keys[index];
115
+ const right = b.keys[index];
116
+ const comparison = left == null && right == null
117
+ ? 0
118
+ : left == null
119
+ ? 1
120
+ : right == null
121
+ ? -1
122
+ : typeof left === 'number' && typeof right === 'number'
123
+ ? left - right
124
+ : collator.compare(String(left), String(right));
125
+ if (comparison)
126
+ return rules[index].direction === 'DESC' ? -comparison : comparison;
127
+ }
128
+ return 0;
129
+ });
130
+ return { data: ordered.slice(start, start + pageSize).map(({ ref }) => ref), total: data.length };
108
131
  }
132
+ /** Проверяет коллекции, значения полей и целостность активных связей.
133
+ * @example Согласованные записи → undefined; обязательная связь без цели → исключение.
134
+ */
109
135
  validateData(data) {
110
136
  if (!this.model.explicit)
111
137
  return;
@@ -116,7 +142,7 @@ export class Engine {
116
142
  const visit = (ref) => {
117
143
  for (const node of Object.values(ref.node.children)) {
118
144
  if (node.relation) {
119
- const matches = related(ref, node);
145
+ const matches = related(ref, node).filter((match) => !match.entity.softDelete || match.value.deletedAt == null);
120
146
  if (!node.many && matches.length > 1)
121
147
  throw domainError('INVALID_INPUT', `Multiple targets for ${ref.entity.name}.${node.path}`);
122
148
  if (node.required && !matches.length)
@@ -139,17 +165,26 @@ export class Engine {
139
165
  for (const entity of this.model.entities)
140
166
  for (const record of data[entity.collection] ?? []) {
141
167
  validateRecord(entity, record, 'stored');
142
- visit(rootRef(context, entity, record));
168
+ if (!entity.softDelete || record.deletedAt == null)
169
+ visit(rootRef(context, entity, record));
143
170
  }
144
171
  }
145
- async mutate(entity, mode, key, body, prepare) {
172
+ /** Выполняет одну операцию записи в транзакции, включая связи, права и подготовку ответа.
173
+ * @example Режим update с { name: 'Анна' } → обновлённая запись; ошибка проверки → прежние данные.
174
+ */
175
+ async mutate(entity, mode, key, body, prepare, actor) {
176
+ const currentActor = () => (typeof actor === 'function' ? actor() : actor);
177
+ if (this.model.options.auth && !currentActor())
178
+ throw domainError('UNAUTHENTICATED', 'Authentication required');
146
179
  if (mode !== 'delete') {
147
180
  if (!isObject(body))
148
181
  throw domainError('INVALID_INPUT', 'Request body must be an object');
149
182
  if (!this.model.explicit && Object.hasOwn(body, 'id'))
150
183
  throw domainError('INVALID_INPUT', 'id is generated and immutable');
151
184
  }
152
- const outcome = await this.store.update((database) => {
185
+ const outcome = await this.store.update((database, before) => {
186
+ // Пока запрос ждал очередь, токен мог быть отозван, а права пользователя — изменены.
187
+ const lifecycle = new RecordMutation(database.data, this.model, currentActor(), before);
153
188
  const finish = (data, record) => {
154
189
  const model = this.modelFor(data);
155
190
  const currentEntity = model.byCollection.get(entity.collection);
@@ -174,34 +209,52 @@ export class Engine {
174
209
  const current = this.find(context, entity, key);
175
210
  if (!current)
176
211
  throw domainError('NOT_FOUND', 'Record not found');
177
- const snapshot = structuredClone(database.data);
178
- this.cascade(context, current);
212
+ lifecycle.check(entity, current.value);
213
+ if (entity.softDelete && current.value.deletedAt != null)
214
+ return finish(database.data, current.value);
215
+ this.cascade(context, current, lifecycle);
216
+ lifecycle.finish();
179
217
  this.validateData(database.data);
180
- return finish(snapshot, current.value);
218
+ return finish(entity.softDelete ? database.data : before, current.value);
181
219
  }
182
- const writer = new MutationWriter(database, this.model, mode === 'replace' ? 'replace' : 'update');
220
+ const writer = new MutationWriter(database, this.model, mode === 'replace' ? 'replace' : 'update', (owner, record) => lifecycle.write(owner, record));
183
221
  const record = writer.write(entity, mode, key, body);
184
222
  writer.validateRelations();
223
+ lifecycle.finish();
185
224
  this.validateData(database.data);
186
225
  return finish(database.data, record);
187
226
  });
188
227
  this.model = outcome.model;
189
228
  return outcome.output;
190
229
  }
191
- cascade(context, initial) {
230
+ /** Вычисляет каскад удаления, проверяет запрещающие связи и меняет только черновик данных.
231
+ * @example Удаление родителя при cascade → удалённые зависимые записи; restrict → исключение.
232
+ */
233
+ cascade(context, initial, lifecycle) {
192
234
  if (!this.model.explicit) {
193
- const rows = context.data[initial.entity.collection];
194
- rows.splice(rows.indexOf(initial.value), 1);
235
+ lifecycle.deleteGroup(initial, new Set([initial.value]));
236
+ if (!initial.entity.softDelete) {
237
+ const rows = context.data[initial.entity.collection];
238
+ rows.splice(rows.indexOf(initial.value), 1);
239
+ }
240
+ lifecycle.capturePruned(initial);
195
241
  return;
196
242
  }
197
243
  const deleted = new Set([initial.value]);
198
244
  const survives = (ref) => !deleted.has(ref.value) && !deleted.has(ref.root) && !Object.values(ref.bindings).some((value) => deleted.has(value));
199
- const blocked = [];
200
245
  const owners = [];
246
+ const dependents = new Map();
201
247
  const collect = (ref) => {
202
248
  for (const node of Object.values(ref.node.children)) {
203
- if (node.relation)
204
- owners.push({ ref, node, targets: related(ref, node) });
249
+ if (node.relation) {
250
+ const owner = { ref, node, targets: related(ref, node) };
251
+ owners.push(owner);
252
+ for (const target of owner.targets) {
253
+ const dependencies = dependents.get(target.value) ?? [];
254
+ dependencies.push(owner);
255
+ dependents.set(target.value, dependencies);
256
+ }
257
+ }
205
258
  else if (node.base === 'object') {
206
259
  const v = resolveField(ref, node);
207
260
  if (isRef(v))
@@ -214,23 +267,23 @@ export class Engine {
214
267
  }
215
268
  };
216
269
  for (const entity of this.model.entities)
217
- this.records(context, entity).forEach(collect);
218
- let changed = true;
219
- while (changed) {
220
- changed = false;
221
- for (const { ref, node, targets } of owners)
222
- if (survives(ref) && targets.some((v) => deleted.has(v.value))) {
223
- if (node.onDelete === 'cascade') {
224
- deleted.add(ref.value);
225
- changed = true;
226
- }
227
- }
270
+ this.records(context, entity)
271
+ .filter((ref) => !entity.softDelete || ref.value.deletedAt == null)
272
+ .forEach(collect);
273
+ const pending = [initial.value];
274
+ for (let index = 0; index < pending.length; index++) {
275
+ for (const { ref, node } of dependents.get(pending[index]) ?? []) {
276
+ if (node.onDelete !== 'cascade' || !survives(ref))
277
+ continue;
278
+ deleted.add(ref.value);
279
+ pending.push(ref.value);
280
+ }
228
281
  }
229
- for (const { ref, node, targets } of owners)
230
- if (survives(ref) && targets.some((v) => deleted.has(v.value)))
231
- blocked.push({ owner: ref.value, root: ref.root, node });
232
- if (blocked.length)
233
- throw domainError('CONFLICT', `Delete restricted by ${blocked[0].node.path}`);
282
+ // restrict проверяем после обхода: ссылающийся объект сам мог попасть в каскад.
283
+ const blocked = owners.find(({ ref, targets }) => survives(ref) && targets.some((target) => deleted.has(target.value)));
284
+ if (blocked)
285
+ throw domainError('CONFLICT', `Delete restricted by ${blocked.node.path}`);
286
+ lifecycle.deleteGroup(initial, deleted);
234
287
  const prune = (node, record) => {
235
288
  for (const [key, child] of Object.entries(node.children))
236
289
  if (!child.relation && child.base === 'object' && record[key] != null) {
@@ -247,11 +300,13 @@ export class Engine {
247
300
  }
248
301
  };
249
302
  for (const entity of this.model.entities) {
250
- context.data[entity.collection] = (context.data[entity.collection] ?? []).filter((record) => !deleted.has(record));
303
+ context.data[entity.collection] = (context.data[entity.collection] ?? []).filter((record) => entity.softDelete || !deleted.has(record));
251
304
  context.data[entity.collection].forEach((record) => {
252
- prune(entity.root, record);
305
+ if (!deleted.has(record))
306
+ prune(entity.root, record);
253
307
  });
254
308
  }
309
+ lifecycle.capturePruned(initial);
255
310
  }
256
311
  }
257
312
  //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/core/engine.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
- export type ErrorCode = 'INVALID_INPUT' | 'INVALID_QUERY' | 'NOT_FOUND' | 'CONFLICT' | 'PAYLOAD_TOO_LARGE' | 'UNAUTHENTICATED' | 'TOO_MANY_REQUESTS';
1
+ export type ErrorCode = 'INVALID_INPUT' | 'INVALID_QUERY' | 'NOT_FOUND' | 'CONFLICT' | 'PAYLOAD_TOO_LARGE' | 'UNAUTHENTICATED' | 'TOO_MANY_REQUESTS' | 'FORBIDDEN';
2
2
  export declare class DomainError extends Error {
3
3
  readonly code: ErrorCode;
4
4
  constructor(code: ErrorCode, message: string);
5
5
  }
6
+ /** Создаёт исключение с отдельным кодом и сообщением.
7
+ * @example domainError('NOT_FOUND', 'Нет записи') → Error с code = 'NOT_FOUND' и message = 'Нет записи'.
8
+ */
6
9
  export declare const domainError: (code: ErrorCode, message: string) => DomainError;
@@ -0,0 +1,12 @@
1
+ export class DomainError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ }
7
+ }
8
+ /** Создаёт исключение с отдельным кодом и сообщением.
9
+ * @example domainError('NOT_FOUND', 'Нет записи') → Error с code = 'NOT_FOUND' и message = 'Нет записи'.
10
+ */
11
+ export const domainError = (code, message) => new DomainError(code, message);
12
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/core/errors.ts"],"names":[],"mappings":"AACA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAEzB,IAAI;IADf,YACW,IAAe,EACxB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;oBAHN,IAAI;IAIf,CAAC;CACF;AACD;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAe,EAAE,OAAe,EAAe,EAAE,CAAC,IAAI,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ErrorCode } from './errors.js';
2
+ export declare const domainStatus: Record<ErrorCode, number>;
3
+ export interface HttpError extends Error {
4
+ statusCode: number;
5
+ }
6
+ /** Создаёт исключение с HTTP-статусом и сообщением.
7
+ * @example createHttpError(404, 'Нет файла') → Error с statusCode = 404 и message = 'Нет файла'.
8
+ */
9
+ export declare const createHttpError: (statusCode: number, message: string) => HttpError;
@@ -0,0 +1,15 @@
1
+ export const domainStatus = {
2
+ INVALID_INPUT: 400,
3
+ INVALID_QUERY: 400,
4
+ NOT_FOUND: 404,
5
+ CONFLICT: 409,
6
+ PAYLOAD_TOO_LARGE: 413,
7
+ UNAUTHENTICATED: 401,
8
+ FORBIDDEN: 403,
9
+ TOO_MANY_REQUESTS: 429,
10
+ };
11
+ /** Создаёт исключение с HTTP-статусом и сообщением.
12
+ * @example createHttpError(404, 'Нет файла') → Error с statusCode = 404 и message = 'Нет файла'.
13
+ */
14
+ export const createHttpError = (statusCode, message) => Object.assign(new Error(message), { statusCode });
15
+ //# sourceMappingURL=http-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-errors.js","sourceRoot":"","sources":["../../../src/core/http-errors.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,YAAY,GAA8B;IACrD,aAAa,EAAE,GAAG;IAClB,aAAa,EAAE,GAAG;IAClB,SAAS,EAAE,GAAG;IACd,QAAQ,EAAE,GAAG;IACb,iBAAiB,EAAE,GAAG;IACtB,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,GAAG;IACd,iBAAiB,EAAE,GAAG;CACvB,CAAC;AAIF;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,UAAkB,EAAE,OAAe,EAAa,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { Entity, Model } from '../model.js';
2
+ import type { DatabaseData, JsonObject } from '../types.js';
3
+ import { type Actor } from './options.js';
4
+ /** Проверяет владельцев и заполняет поля дат и авторов внутри транзакции. */
5
+ export declare class RecordMutation {
6
+ private data;
7
+ private model;
8
+ private actor;
9
+ readonly before: Readonly<DatabaseData>;
10
+ readonly now: string;
11
+ private originals;
12
+ private touched;
13
+ private enabled;
14
+ constructor(data: DatabaseData, model: Model, actor: Actor | undefined, before: Readonly<DatabaseData>);
15
+ /** Проверяет владельца исходной записи или права администратора; данные не меняет.
16
+ * @example Совпадающие createdById и actor.id → undefined; чужая запись без прав администратора → FORBIDDEN.
17
+ */
18
+ check(entity: Entity, record: JsonObject): void;
19
+ /** Отмечает запись изменённой и при необходимости восстанавливает её каскад в черновике.
20
+ * @example Удалённая запись после успешного вызова → deletedAt: null; конфликт восстановления → ошибка.
21
+ */
22
+ write(entity: Entity, record: JsonObject): void;
23
+ /** Очищает признаки удаления и служебные сведения восстановления, отмечая запись изменённой.
24
+ * @example Запись с deletedAt и сведениями каскада → deletedAt: null без служебных сведений; результат undefined.
25
+ */
26
+ private clear;
27
+ /** Помечает выбранные записи временем удаления и общим случайным идентификатором операции.
28
+ * @example Две записи с включённым мягким удалением → одинаковые дата и идентификатор каскада.
29
+ */
30
+ deleteGroup(initial: {
31
+ entity: Entity;
32
+ value: JsonObject;
33
+ }, deleted: Set<JsonObject>): void;
34
+ /** Сохраняет изменения вложенных объектов для последующего восстановления каскада.
35
+ * @example Поле actors изменилось с [a, b] на [b] → в сведениях удаления сохранены оба состояния.
36
+ */
37
+ capturePruned(initial: {
38
+ entity: Entity;
39
+ value: JsonObject;
40
+ }): void;
41
+ /** Проверяет права на все изменения и проставляет даты и авторов в черновике.
42
+ * @example Новая запись при включённых флагах → createdAt = updatedAt и createdById = updatedById.
43
+ */
44
+ finish(): void;
45
+ /** Разбирает и проверяет внутреннюю JSON-строку сведений об удалении.
46
+ * @example Отсутствующие сведения → undefined; корректная строка → объект; повреждённая строка → ошибка.
47
+ */
48
+ private deletion;
49
+ }
@@ -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, before) {
17
+ this.data = data;
18
+ this.model = model;
19
+ this.actor = actor;
20
+ this.before = before;
21
+ if (model.options.auth && !actor)
22
+ throw domainError('UNAUTHENTICATED', 'Authentication required');
23
+ this.enabled = model.options.auth || model.entities.some((entity) => entity.timestamps || entity.softDelete);
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