@kollors/deep-json-server 0.9.0 → 1.0.0-alpha.10

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 (229) hide show
  1. package/README.md +566 -347
  2. package/README.ru.md +560 -341
  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 +13 -5
  6. package/dist/index.js +4 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +31 -0
  9. package/dist/src/auth/contract.js +12 -0
  10. package/dist/src/auth/contract.js.map +1 -0
  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 +17 -0
  15. package/dist/src/auth/password.js +41 -0
  16. package/dist/src/auth/password.js.map +1 -0
  17. package/dist/src/auth/public.d.ts +3 -0
  18. package/dist/src/auth/public.js +5 -0
  19. package/dist/src/auth/public.js.map +1 -0
  20. package/dist/src/auth/routes.d.ts +6 -0
  21. package/dist/src/auth/routes.js +18 -0
  22. package/dist/src/auth/routes.js.map +1 -0
  23. package/dist/src/auth/service.d.ts +78 -0
  24. package/dist/src/auth/service.js +218 -0
  25. package/dist/src/auth/service.js.map +1 -0
  26. package/dist/src/auth/store.d.ts +26 -0
  27. package/dist/src/auth/store.js +82 -0
  28. package/dist/src/auth/store.js.map +1 -0
  29. package/dist/src/cli/index.d.ts +7 -0
  30. package/dist/src/cli/index.js +122 -0
  31. package/dist/src/cli/index.js.map +1 -0
  32. package/dist/src/core/config-values.d.ts +28 -0
  33. package/dist/src/core/config-values.js +53 -0
  34. package/dist/src/core/config-values.js.map +1 -0
  35. package/dist/src/{constants.d.ts → core/constants.d.ts} +2 -1
  36. package/dist/src/{constants.js → core/constants.js} +2 -1
  37. package/dist/src/core/constants.js.map +1 -0
  38. package/dist/src/core/database.d.ts +48 -0
  39. package/dist/src/{database.js → core/database.js} +64 -29
  40. package/dist/src/core/database.js.map +1 -0
  41. package/dist/src/core/engine.d.ts +65 -0
  42. package/dist/src/core/engine.js +312 -0
  43. package/dist/src/core/engine.js.map +1 -0
  44. package/dist/src/core/errors.d.ts +9 -0
  45. package/dist/src/core/errors.js +12 -0
  46. package/dist/src/core/errors.js.map +1 -0
  47. package/dist/src/core/http-errors.d.ts +9 -0
  48. package/dist/src/core/http-errors.js +15 -0
  49. package/dist/src/core/http-errors.js.map +1 -0
  50. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  51. package/dist/src/core/lifecycle/mutation.js +170 -0
  52. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  53. package/dist/src/core/lifecycle/options.d.ts +21 -0
  54. package/dist/src/core/lifecycle/options.js +19 -0
  55. package/dist/src/core/lifecycle/options.js.map +1 -0
  56. package/dist/src/core/model.d.ts +145 -0
  57. package/dist/src/core/model.js +510 -0
  58. package/dist/src/core/model.js.map +1 -0
  59. package/dist/src/core/mutations/write.d.ts +26 -0
  60. package/dist/src/core/mutations/write.js +284 -0
  61. package/dist/src/core/mutations/write.js.map +1 -0
  62. package/dist/src/core/operations.d.ts +33 -0
  63. package/dist/src/core/operations.js +10 -0
  64. package/dist/src/core/operations.js.map +1 -0
  65. package/dist/src/core/pagination.d.ts +10 -0
  66. package/dist/src/core/pagination.js +13 -0
  67. package/dist/src/core/pagination.js.map +1 -0
  68. package/dist/src/core/paths.d.ts +16 -0
  69. package/dist/src/core/paths.js +64 -0
  70. package/dist/src/core/paths.js.map +1 -0
  71. package/dist/src/core/query/contract.d.ts +6 -0
  72. package/dist/src/core/query/contract.js +22 -0
  73. package/dist/src/core/query/contract.js.map +1 -0
  74. package/dist/src/core/query/filter.d.ts +6 -0
  75. package/dist/src/core/query/filter.js +100 -0
  76. package/dist/src/core/query/filter.js.map +1 -0
  77. package/dist/src/core/query/options.d.ts +30 -0
  78. package/dist/src/core/query/options.js +44 -0
  79. package/dist/src/core/query/options.js.map +1 -0
  80. package/dist/src/core/records.d.ts +46 -0
  81. package/dist/src/core/records.js +69 -0
  82. package/dist/src/core/records.js.map +1 -0
  83. package/dist/src/core/relation-metadata.d.ts +15 -0
  84. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -2
  85. package/dist/src/core/relation-metadata.js.map +1 -0
  86. package/dist/src/core/storage.d.ts +2 -0
  87. package/dist/src/core/storage.js +2 -0
  88. package/dist/src/core/storage.js.map +1 -0
  89. package/dist/src/core/types.d.ts +10 -0
  90. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  91. package/dist/src/core/utils.d.ts +56 -0
  92. package/dist/src/core/utils.js +102 -0
  93. package/dist/src/core/utils.js.map +1 -0
  94. package/dist/src/files/contract.d.ts +32 -30
  95. package/dist/src/files/contract.js +29 -48
  96. package/dist/src/files/contract.js.map +1 -1
  97. package/dist/src/files/disk-store.d.ts +5 -1
  98. package/dist/src/files/disk-store.js +61 -33
  99. package/dist/src/files/disk-store.js.map +1 -1
  100. package/dist/src/files/http.d.ts +36 -0
  101. package/dist/src/files/http.js +53 -0
  102. package/dist/src/files/http.js.map +1 -0
  103. package/dist/src/files/index.d.ts +5 -4
  104. package/dist/src/files/index.js +7 -2
  105. package/dist/src/files/index.js.map +1 -1
  106. package/dist/src/files/memory-store.d.ts +4 -1
  107. package/dist/src/files/memory-store.js +29 -22
  108. package/dist/src/files/memory-store.js.map +1 -1
  109. package/dist/src/files/routes.d.ts +3 -0
  110. package/dist/src/files/routes.js +27 -2
  111. package/dist/src/files/routes.js.map +1 -1
  112. package/dist/src/files/streams.d.ts +5 -0
  113. package/dist/src/files/streams.js +20 -0
  114. package/dist/src/files/streams.js.map +1 -0
  115. package/dist/src/graphql/entry.d.ts +12 -0
  116. package/dist/src/graphql/entry.js +13 -0
  117. package/dist/src/graphql/entry.js.map +1 -0
  118. package/dist/src/graphql/generate.d.ts +12 -0
  119. package/dist/src/graphql/generate.js +19 -0
  120. package/dist/src/graphql/generate.js.map +1 -0
  121. package/dist/src/graphql/preflight.d.ts +6 -0
  122. package/dist/src/graphql/preflight.js +42 -0
  123. package/dist/src/graphql/preflight.js.map +1 -0
  124. package/dist/src/graphql/resolvers.d.ts +17 -0
  125. package/dist/src/graphql/resolvers.js +44 -0
  126. package/dist/src/graphql/resolvers.js.map +1 -0
  127. package/dist/src/graphql/routes.d.ts +8 -0
  128. package/dist/src/graphql/routes.js +30 -0
  129. package/dist/src/graphql/routes.js.map +1 -0
  130. package/dist/src/graphql/schema.d.ts +6 -0
  131. package/dist/src/graphql/schema.js +230 -0
  132. package/dist/src/graphql/schema.js.map +1 -0
  133. package/dist/src/graphql/write.d.ts +4 -0
  134. package/dist/src/graphql/write.js +10 -0
  135. package/dist/src/graphql/write.js.map +1 -0
  136. package/dist/src/openapi/auth.d.ts +13 -0
  137. package/dist/src/openapi/auth.js +139 -0
  138. package/dist/src/openapi/auth.js.map +1 -0
  139. package/dist/src/openapi/document.d.ts +11 -7
  140. package/dist/src/openapi/document.js +287 -318
  141. package/dist/src/openapi/document.js.map +1 -1
  142. package/dist/src/openapi/entry.d.ts +14 -0
  143. package/dist/src/openapi/entry.js +13 -0
  144. package/dist/src/openapi/entry.js.map +1 -0
  145. package/dist/src/openapi/files.d.ts +6 -0
  146. package/dist/src/openapi/files.js +87 -0
  147. package/dist/src/openapi/files.js.map +1 -0
  148. package/dist/src/openapi/generate.d.ts +19 -0
  149. package/dist/src/openapi/generate.js +41 -0
  150. package/dist/src/openapi/generate.js.map +1 -0
  151. package/dist/src/openapi/helpers.d.ts +26 -0
  152. package/dist/src/openapi/helpers.js +13 -0
  153. package/dist/src/openapi/helpers.js.map +1 -0
  154. package/dist/src/openapi/options.d.ts +18 -0
  155. package/dist/src/openapi/options.js +13 -0
  156. package/dist/src/openapi/options.js.map +1 -0
  157. package/dist/src/{types.d.ts → openapi/types.d.ts} +2 -12
  158. package/dist/src/openapi/types.js +2 -0
  159. package/dist/src/openapi/types.js.map +1 -0
  160. package/dist/src/openapi/write.d.ts +5 -0
  161. package/dist/src/openapi/write.js +12 -0
  162. package/dist/src/openapi/write.js.map +1 -0
  163. package/dist/src/rest/options.d.ts +22 -0
  164. package/dist/src/rest/options.js +65 -0
  165. package/dist/src/rest/options.js.map +1 -0
  166. package/dist/src/rest/projection.d.ts +13 -0
  167. package/dist/src/rest/projection.js +61 -0
  168. package/dist/src/rest/projection.js.map +1 -0
  169. package/dist/src/rest/routes.d.ts +7 -0
  170. package/dist/src/rest/routes.js +49 -0
  171. package/dist/src/rest/routes.js.map +1 -0
  172. package/dist/src/server/config.d.ts +74 -0
  173. package/dist/src/server/config.js +142 -0
  174. package/dist/src/server/config.js.map +1 -0
  175. package/dist/src/server/create.d.ts +16 -0
  176. package/dist/src/server/create.js +133 -0
  177. package/dist/src/server/create.js.map +1 -0
  178. package/dist/src/server/features.d.ts +4 -0
  179. package/dist/src/server/features.js +11 -0
  180. package/dist/src/server/features.js.map +1 -0
  181. package/dist/src/server/model.d.ts +6 -0
  182. package/dist/src/server/model.js +14 -0
  183. package/dist/src/server/model.js.map +1 -0
  184. package/dist/src/server/public.d.ts +2 -0
  185. package/dist/src/server/public.js +2 -0
  186. package/dist/src/server/public.js.map +1 -0
  187. package/package.json +25 -4
  188. package/dist/src/cli.d.ts +0 -4
  189. package/dist/src/cli.js +0 -80
  190. package/dist/src/cli.js.map +0 -1
  191. package/dist/src/config.d.ts +0 -54
  192. package/dist/src/config.js +0 -145
  193. package/dist/src/config.js.map +0 -1
  194. package/dist/src/constants.js.map +0 -1
  195. package/dist/src/database.d.ts +0 -20
  196. package/dist/src/database.js.map +0 -1
  197. package/dist/src/openapi/config.d.ts +0 -11
  198. package/dist/src/openapi/config.js +0 -204
  199. package/dist/src/openapi/config.js.map +0 -1
  200. package/dist/src/openapi/index.d.ts +0 -10
  201. package/dist/src/openapi/index.js +0 -27
  202. package/dist/src/openapi/index.js.map +0 -1
  203. package/dist/src/openapi/inference.d.ts +0 -14
  204. package/dist/src/openapi/inference.js +0 -145
  205. package/dist/src/openapi/inference.js.map +0 -1
  206. package/dist/src/query/filter.d.ts +0 -6
  207. package/dist/src/query/filter.js +0 -248
  208. package/dist/src/query/filter.js.map +0 -1
  209. package/dist/src/query/index.d.ts +0 -3
  210. package/dist/src/query/index.js +0 -4
  211. package/dist/src/query/index.js.map +0 -1
  212. package/dist/src/query/pagination.d.ts +0 -11
  213. package/dist/src/query/pagination.js +0 -28
  214. package/dist/src/query/pagination.js.map +0 -1
  215. package/dist/src/query/sort.d.ts +0 -1
  216. package/dist/src/query/sort.js +0 -54
  217. package/dist/src/query/sort.js.map +0 -1
  218. package/dist/src/relation-metadata.d.ts +0 -10
  219. package/dist/src/relation-metadata.js.map +0 -1
  220. package/dist/src/relations.d.ts +0 -12
  221. package/dist/src/relations.js +0 -155
  222. package/dist/src/relations.js.map +0 -1
  223. package/dist/src/server.d.ts +0 -13
  224. package/dist/src/server.js +0 -188
  225. package/dist/src/server.js.map +0 -1
  226. package/dist/src/utils.d.ts +0 -20
  227. package/dist/src/utils.js +0 -63
  228. package/dist/src/utils.js.map +0 -1
  229. /package/dist/src/{types.js → core/types.js} +0 -0
@@ -1,338 +1,307 @@
1
- import { DEFAULT_MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE } from '../constants.js';
2
- import { validateDatabase } from '../database.js';
3
- import { FILE_HEADERS, FILE_METADATA_SCHEMA, FILE_ROUTES, FILE_UPDATE_SCHEMA } from '../files/contract.js';
4
- import { getRelationMetadata } from '../relation-metadata.js';
5
- import { getResourceNames, isObject, singularize, toPascalCase } from '../utils.js';
6
- import { applyConfiguredFields, normalizeSchemaConfig } from './config.js';
7
- import { ensureGeneratedIdSchema, inferObjectSchema, mergeSchemaOverrides, omitId } from './inference.js';
8
- const createSchemaReference = (name) => ({ $ref: `#/components/schemas/${name}` });
9
- const addForwardRelations = (schema, resources, componentNames, sourceResource) => {
10
- if (Array.isArray(schema.oneOf)) {
11
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => addForwardRelations(nestedSchema, resources, componentNames, sourceResource)) };
1
+ import { VERSION } from '../core/constants.js';
2
+ import { assertApi, canonicalNode, nodeName, objectSchema, operationName, relationInputSchema, valueSchema } from '../core/model.js';
3
+ import { MUTATIONS } from '../core/operations.js';
4
+ import { normalizePagination } from '../core/pagination.js';
5
+ import { operatorsFor } from '../core/query/contract.js';
6
+ import { sortableFields } from '../core/query/options.js';
7
+ import { capitalize, isObject } from '../core/utils.js';
8
+ import { FILE_HEADERS, FILE_METADATA_SCHEMA, FILE_UPDATE_SCHEMA } from '../files/http.js';
9
+ import { AUTH_SCHEMAS, AUTH_SECURITY_SCHEMES, authOpenapiPaths } from './auth.js';
10
+ import { createFilePaths } from './files.js';
11
+ import { json, ref, response } from './helpers.js';
12
+ /** Преобразует nullable-значения JSON Schema в представление OpenAPI 3.0, рекурсивно обрабатывая поля и элементы.
13
+ * @example { anyOf: [{ type: 'string' }, { type: 'null' }] } → { type: 'string', nullable: true }.
14
+ */
15
+ function toOpenapi(schema) {
16
+ if (Array.isArray(schema.anyOf) && schema.anyOf.some((v) => isObject(v) && v.type === 'null')) {
17
+ const nonNull = schema.anyOf.find((v) => isObject(v) && v.type !== 'null');
18
+ const converted = toOpenapi(nonNull);
19
+ if (!converted.type)
20
+ return { anyOf: [converted, { type: 'object', nullable: true, enum: [null] }] };
21
+ return { ...converted, nullable: true, ...(converted.enum ? { enum: [...converted.enum.filter((value) => value !== null), null] } : {}) };
12
22
  }
13
- if (schema.type === 'array') {
14
- return { ...schema, items: addForwardRelations(schema.items ?? {}, resources, componentNames, sourceResource) };
23
+ const result = { ...schema };
24
+ if (isObject(schema.properties))
25
+ result.properties = Object.fromEntries(Object.entries(schema.properties).map(([k, v]) => [k, toOpenapi(v)]));
26
+ if (isObject(schema.items))
27
+ result.items = toOpenapi(schema.items);
28
+ return result;
29
+ }
30
+ /** Строит документ с маршрутами и схемами записей, включая выбранные дополнительные маршруты.
31
+ * @example { model, files: true } → документ с paths['/_files/storage'].
32
+ */
33
+ export function buildOpenapiDocument({ model, files = false, auth = false, pageSize, maxPageSize, info = { title: 'Deep JSON Server API', version: VERSION }, }) {
34
+ assertApi(model, 'openapi');
35
+ ({ pageSize, maxPageSize } = normalizePagination({ pageSize, maxPageSize }));
36
+ const schemas = {
37
+ Error: { type: 'object', properties: { error: { type: 'string' } }, required: ['error'] },
38
+ Pager: {
39
+ type: 'object',
40
+ additionalProperties: false,
41
+ properties: { page: { type: 'integer', minimum: 1, default: 1 }, pageSize: { type: 'integer', minimum: 1, maximum: maxPageSize, default: pageSize } },
42
+ },
43
+ };
44
+ const owners = new Map();
45
+ function reserve(name, node) {
46
+ if (owners.get(name) === node)
47
+ return false;
48
+ if (schemas[name])
49
+ throw new Error(`OpenAPI schema name collision: ${name}`);
50
+ owners.set(name, node);
51
+ schemas[name] = {};
52
+ return true;
15
53
  }
16
- if (schema.type !== 'object' || !isObject(schema.properties)) {
54
+ function annotate(schema, node) {
55
+ for (const key of ['description', 'example', 'default', 'readOnly', 'writeOnly'])
56
+ if (node[key] !== undefined)
57
+ schema[key] = node[key];
58
+ if (node.generated)
59
+ schema.readOnly = true;
17
60
  return schema;
18
61
  }
19
- const properties = Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, addForwardRelations(value, resources, componentNames, sourceResource)]));
20
- Object.keys(schema.properties).forEach((key) => {
21
- const relation = getRelationMetadata(key, resources, sourceResource);
22
- if (relation != null) {
23
- const relationSchema = createSchemaReference(componentNames[relation.targetResource]);
24
- properties[relation.relationName] = relation.isMany ? { items: relationSchema, type: 'array' } : relationSchema;
62
+ function baseField(node) {
63
+ return annotate(toOpenapi(valueSchema(node)), node);
64
+ }
65
+ function annotateInput(schema, node, defaults) {
66
+ for (const [key, property] of Object.entries(schema.properties ?? {})) {
67
+ const child = node.children[key];
68
+ for (const attribute of ['description', 'example', 'writeOnly'])
69
+ if (child[attribute] !== undefined)
70
+ property[attribute] = child[attribute];
71
+ if (defaults && child.default !== undefined)
72
+ property.default = child.default;
73
+ if (!child.relation && child.base === 'object')
74
+ annotateInput(child.many ? property.items : property, child, defaults);
25
75
  }
26
- });
27
- return { ...schema, properties };
28
- };
29
- const collectRelations = (schema, resources, sourceResource, relations = []) => {
30
- if (Array.isArray(schema.oneOf)) {
31
- schema.oneOf.forEach((nestedSchema) => {
32
- collectRelations(nestedSchema, resources, sourceResource, relations);
33
- });
34
- return relations;
76
+ return schema;
77
+ }
78
+ function writeInput(node, mode, root = false, nestedMode = mode) {
79
+ return annotateInput(toOpenapi(objectSchema(node, mode, root, (child) => relationInputSchema(child, nestedInput(child.relation, nestedMode)))), node, mode !== 'update');
35
80
  }
36
- if (schema.type === 'array') {
37
- collectRelations(schema.items ?? {}, resources, sourceResource, relations);
38
- return relations;
81
+ function nestedInput(entity, mode) {
82
+ const name = `${entity.name}Nested${capitalize(mode)}`;
83
+ if (!reserve(name, entity.root))
84
+ return ref(name);
85
+ const existing = writeInput(entity.root, mode === 'replace' ? 'replace' : 'update', true, mode);
86
+ existing.properties ??= {};
87
+ existing.properties[entity.primary] = toOpenapi(valueSchema(entity.fields[entity.primary]));
88
+ existing.required = [...(existing.required ?? []), entity.primary];
89
+ const variants = [existing];
90
+ if (entity.fields[entity.primary].generated)
91
+ variants.push(writeInput(entity.root, 'create', true, mode));
92
+ schemas[name] = { anyOf: variants, description: 'An object with a primary key updates an existing record; an object without a key creates one. PUT replaces, PATCH updates supplied fields.' };
93
+ return ref(name);
39
94
  }
40
- if (schema.type !== 'object' || !isObject(schema.properties)) {
41
- return relations;
95
+ function output(entity, node) {
96
+ [entity, node] = canonicalNode(entity, node);
97
+ const name = nodeName(entity, node);
98
+ if (!reserve(name, node))
99
+ return ref(name);
100
+ const properties = {};
101
+ for (const [key, child] of Object.entries(node.children)) {
102
+ if (child.writeOnly)
103
+ continue;
104
+ if (child.relation || child.base === 'object') {
105
+ const value = child.many ? page(entity, child) : output(entity, child);
106
+ const shape = child.nullable || (child.relation && !child.many && !child.required) ? { anyOf: [value, { type: 'object', nullable: true, enum: [null] }] } : value;
107
+ const attributes = annotate({}, child);
108
+ properties[key] = Object.keys(attributes).length ? { allOf: [shape], ...attributes } : shape;
109
+ }
110
+ else
111
+ properties[key] = baseField(child);
112
+ }
113
+ // Выбор может содержать любую часть полей, поэтому поля ответа необязательны.
114
+ schemas[name] = { type: 'object', additionalProperties: false, properties };
115
+ return ref(name);
42
116
  }
43
- Object.entries(schema.properties).forEach(([key, value]) => {
44
- const relation = getRelationMetadata(key, resources, sourceResource);
45
- if (relation != null) {
46
- relations.push(relation);
117
+ function scope(entity, node, list = node.many) {
118
+ [entity, node] = canonicalNode(entity, node);
119
+ const name = `${nodeName(entity, node)}${list ? 'List' : ''}Scope`;
120
+ if (!reserve(name, node))
121
+ return ref(name);
122
+ const fieldsName = `${nodeName(entity, node)}ScopeFields`;
123
+ if (reserve(fieldsName, node)) {
124
+ const included = { type: 'boolean', enum: [true] };
125
+ const properties = { '*': included };
126
+ for (const [key, child] of Object.entries(node.children)) {
127
+ if (child.writeOnly)
128
+ continue;
129
+ properties[key] = child.relation || child.base === 'object' ? scope(entity, child) : included;
130
+ }
131
+ schemas[fieldsName] = { type: 'object', additionalProperties: false, properties };
47
132
  }
48
- collectRelations(value, resources, sourceResource, relations);
49
- });
50
- return relations;
51
- };
52
- const addReverseRelations = (schemas, rawSchemas, resources, componentNames) => {
53
- resources.forEach((sourceResource) => {
54
- collectRelations(rawSchemas[sourceResource], resources, sourceResource).forEach(({ reverseRelationName, targetResource }) => {
55
- const targetComponentName = componentNames[targetResource];
56
- const targetSchema = schemas[targetComponentName];
57
- if (targetSchema.type === 'object' && isObject(targetSchema.properties) && !Object.hasOwn(targetSchema.properties, reverseRelationName)) {
58
- schemas[targetComponentName] = {
59
- ...targetSchema,
60
- properties: {
61
- ...targetSchema.properties,
62
- [reverseRelationName]: { items: createSchemaReference(componentNames[sourceResource]), type: 'array' },
63
- },
64
- };
133
+ schemas[name] = {
134
+ type: 'array',
135
+ minItems: 1,
136
+ maxItems: list ? 2 : 1,
137
+ items: list ? { anyOf: [ref(fieldsName), options(entity, node)] } : ref(fieldsName),
138
+ description: list
139
+ ? '[fields, arguments?]. The first object selects fields; the optional second object contains where, order and pager. OpenAPI 3.0 cannot express positional item schemas; the server validates their order.'
140
+ : '[fields]. * selects own fields without relations or writeOnly fields.',
141
+ };
142
+ return ref(name);
143
+ }
144
+ function page(entity, node) {
145
+ [entity, node] = canonicalNode(entity, node);
146
+ const name = `${nodeName(entity, node)}Page`;
147
+ if (reserve(name, node))
148
+ schemas[name] = { type: 'object', required: ['data', 'total'], properties: { data: { type: 'array', items: output(entity, node) }, total: { type: 'integer', minimum: 0 } } };
149
+ return ref(name);
150
+ }
151
+ function where(entity, node) {
152
+ [entity, node] = canonicalNode(entity, node);
153
+ const name = `${nodeName(entity, node)}Where`;
154
+ if (!reserve(name, node))
155
+ return ref(name);
156
+ const properties = { and: { type: 'array', items: ref(name) }, or: { type: 'array', minItems: 1, items: ref(name) }, not: ref(name) };
157
+ for (const [key, child] of Object.entries(node.children))
158
+ if (!child.writeOnly) {
159
+ if (Object.hasOwn(properties, key))
160
+ throw new Error(`Reserved filter field: ${entity.name}.${child.path}`);
161
+ properties[key] = !child.many && (child.relation || child.base === 'object') ? where(entity, child) : filter(entity, child);
65
162
  }
163
+ schemas[name] = { type: 'object', additionalProperties: false, properties };
164
+ return ref(name);
165
+ }
166
+ function filter(entity, node) {
167
+ const name = `${nodeName(entity, node)}Filter`;
168
+ if (!reserve(name, node))
169
+ return ref(name);
170
+ const properties = {};
171
+ const element = node.many ? (node.relation || node.base === 'object' ? where(entity, node) : filter(entity, { ...node, many: false, path: `${node.path}_element` })) : undefined;
172
+ for (const [key, operand] of Object.entries(operatorsFor(node))) {
173
+ const nullable = ['eq', 'ne', 'in'].includes(key);
174
+ const scalar = {
175
+ type: node.base,
176
+ ...(nullable ? { nullable: true } : {}),
177
+ ...(node.enum ? { enum: [...node.enum, ...(nullable && !node.enum.includes(null) ? [null] : [])] } : {}),
178
+ };
179
+ if (operand === 'condition')
180
+ properties[key] = ref(name);
181
+ else if (operand === 'element')
182
+ properties[key] = element;
183
+ else
184
+ properties[key] =
185
+ operand === 'values' ? { type: 'array', items: scalar } : operand === 'text' ? { type: 'string' } : operand === 'comparison' ? { type: node.base } : scalar;
186
+ }
187
+ schemas[name] = { type: 'object', additionalProperties: false, properties };
188
+ return ref(name);
189
+ }
190
+ function order(entity, node) {
191
+ [entity, node] = canonicalNode(entity, node);
192
+ const name = `${nodeName(entity, node)}Order`;
193
+ if (reserve(name, node)) {
194
+ const paths = sortableFields(node);
195
+ schemas[name] = {
196
+ type: 'array',
197
+ ...(paths.length
198
+ ? {
199
+ items: {
200
+ type: 'object',
201
+ additionalProperties: false,
202
+ required: ['field', 'direction'],
203
+ properties: { field: { type: 'string', enum: paths }, direction: { type: 'string', enum: ['ASC', 'DESC'] } },
204
+ },
205
+ }
206
+ : { maxItems: 0, items: { type: 'object' } }),
207
+ };
208
+ }
209
+ return ref(name);
210
+ }
211
+ function options(entity, node) {
212
+ return { type: 'object', additionalProperties: false, properties: { where: where(entity, node), order: order(entity, node), pager: ref('Pager') } };
213
+ }
214
+ const paths = {};
215
+ const operations = new Set();
216
+ for (const entity of model.entities.filter((e) => e.api.includes('openapi'))) {
217
+ const name = entity.name;
218
+ const op = operationName(entity);
219
+ for (const { mode, hasBody } of MUTATIONS) {
220
+ if (!hasBody)
221
+ continue;
222
+ const key = `${name}${capitalize(mode)}`;
223
+ reserve(key, entity.root);
224
+ schemas[key] = writeInput(entity.root, mode, true);
225
+ }
226
+ const selectionParameter = (list) => ({
227
+ in: 'query',
228
+ name: 'scope',
229
+ ...json(scope(entity, entity.root, list)),
230
+ description: 'JSON [fields, arguments?]. Scalars use true; objects and relations use their own scope arrays. * includes own fields without relations or writeOnly fields. Arguments are available only on lists.',
66
231
  });
67
- });
68
- };
69
- const createParameters = (maxPageSize) => ({
70
- ContentDirectory: { description: 'URI-encoded relative storage directory', in: 'header', name: FILE_HEADERS.directory.name, schema: { type: 'string' } },
71
- ContentName: { description: 'URI-encoded file name', in: 'header', name: FILE_HEADERS.name.name, required: true, schema: { type: 'string' } },
72
- ContentOverride: {
73
- description: 'Overwrite an existing file at the same path',
74
- in: 'header',
75
- name: FILE_HEADERS.override.name,
76
- schema: { default: 'false', enum: ['false', 'true'], type: 'string' },
77
- },
78
- Embed: { description: 'Relationship paths to embed', explode: true, in: 'query', name: '_embed', schema: { items: { type: 'string' }, type: 'array' }, style: 'form' },
79
- FilePath: { description: 'Percent-encoded file path relative to the storage directory', in: 'path', name: 'path', required: true, schema: { type: 'string' } },
80
- Id: { in: 'path', name: 'id', required: true, schema: { type: 'string' } },
81
- Page: { in: 'query', name: '_page', required: false, schema: { default: 1, minimum: 1, type: 'integer' } },
82
- PerPage: { in: 'query', name: '_perPage', required: false, schema: { default: DEFAULT_PAGE_SIZE, maximum: maxPageSize, minimum: 1, type: 'integer' } },
83
- Sort: { description: 'Comma-separated fields; prefix with - for descending order', in: 'query', name: '_sort', schema: { type: 'string' } },
84
- Where: { description: 'JSON-encoded filter for nested data', in: 'query', name: '_where', schema: { type: 'string' } },
85
- });
86
- const createJsonContent = (schema) => ({ content: { 'application/json': { schema } } });
87
- const createResponse = (description, schema) => ({ description, ...(schema != null && createJsonContent(schema)) });
88
- const createErrorResponse = (description) => createResponse(description, createSchemaReference('Error'));
89
- const createParameterReference = (name) => ({ $ref: `#/components/parameters/${name}` });
90
- const createRequestBody = (name) => ({ required: true, ...createJsonContent(createSchemaReference(name)) });
91
- const createFilePaths = () => ({
92
- [`${FILE_ROUTES.download}/{path}`]: {
93
- get: {
94
- operationId: 'downloadFile',
95
- parameters: [createParameterReference('FilePath')],
96
- responses: {
97
- 200: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, description: 'File download' },
98
- 400: createErrorResponse('Invalid path'),
99
- 404: createErrorResponse('Not found'),
100
- },
101
- tags: ['files'],
102
- },
103
- },
104
- [`${FILE_ROUTES.metadata}/{path}`]: {
105
- get: {
106
- operationId: 'getFileMetadata',
107
- parameters: [createParameterReference('FilePath')],
108
- responses: {
109
- 200: createResponse('File metadata', createSchemaReference('FileMetadata')),
110
- 400: createErrorResponse('Invalid path'),
111
- 404: createErrorResponse('Not found'),
112
- },
113
- tags: ['files'],
114
- },
115
- },
116
- [FILE_ROUTES.storage]: {
117
- post: {
118
- operationId: 'uploadFile',
119
- parameters: ['ContentName', 'ContentDirectory', 'ContentOverride'].map(createParameterReference),
120
- requestBody: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, required: true },
121
- responses: {
122
- 200: createResponse('Overwritten', createSchemaReference('FileMetadata')),
123
- 201: createResponse('Created', createSchemaReference('FileMetadata')),
124
- 400: createErrorResponse('Invalid request'),
125
- 409: createErrorResponse('Already exists'),
126
- 413: createErrorResponse('File is too large'),
127
- 415: createErrorResponse('Unsupported media type'),
128
- },
129
- tags: ['files'],
130
- },
131
- },
132
- [`${FILE_ROUTES.storage}/{path}`]: {
133
- delete: {
134
- operationId: 'deleteFile',
135
- parameters: [createParameterReference('FilePath')],
136
- responses: {
137
- 204: { description: 'Deleted' },
138
- 400: createErrorResponse('Invalid path'),
139
- 404: createErrorResponse('Not found'),
140
- },
141
- tags: ['files'],
142
- },
143
- get: {
144
- operationId: 'getFileContent',
145
- parameters: [createParameterReference('FilePath')],
146
- responses: {
147
- 200: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, description: 'File contents' },
148
- 400: createErrorResponse('Invalid path'),
149
- 404: createErrorResponse('Not found'),
150
- },
151
- tags: ['files'],
152
- },
153
- patch: {
154
- operationId: 'updateFile',
155
- parameters: [createParameterReference('FilePath')],
156
- requestBody: createRequestBody('FileUpdate'),
157
- responses: {
158
- 200: createResponse('Updated', createSchemaReference('FileMetadata')),
159
- 400: createErrorResponse('Invalid request'),
160
- 404: createErrorResponse('Not found'),
161
- 409: createErrorResponse('Already exists'),
162
- 413: createErrorResponse('Request is too large'),
163
- 415: createErrorResponse('Unsupported media type'),
164
- },
165
- tags: ['files'],
166
- },
167
- },
168
- });
169
- const createResourceOperationIds = (resource) => {
170
- const resourceName = toPascalCase(resource);
171
- return {
172
- create: `post${resourceName}`,
173
- get: `get${resourceName}ById`,
174
- list: `get${resourceName}`,
175
- remove: `delete${resourceName}ById`,
176
- replace: `put${resourceName}ById`,
177
- update: `patch${resourceName}ById`,
178
- };
179
- };
180
- const createResourcePaths = (resource, componentName) => {
181
- const operationIds = createResourceOperationIds(resource);
182
- return {
183
- [`/${resource}`]: {
184
- get: {
185
- operationId: operationIds.list,
186
- parameters: ['Page', 'PerPage', 'Sort', 'Where', 'Embed'].map(createParameterReference),
187
- responses: { 200: createResponse('Successful response', createSchemaReference(`${componentName}Page`)), 400: createErrorResponse('Invalid query') },
188
- tags: [resource],
189
- },
190
- post: {
191
- operationId: operationIds.create,
192
- requestBody: createRequestBody(`${componentName}Create`),
193
- responses: { 201: createResponse('Created', createSchemaReference(componentName)), 400: createErrorResponse('Invalid request') },
194
- tags: [resource],
195
- },
196
- },
197
- [`/${resource}/{id}`]: {
198
- delete: {
199
- operationId: operationIds.remove,
200
- parameters: [createParameterReference('Id')],
201
- responses: { 200: createResponse('Deleted', createSchemaReference(componentName)), 404: createErrorResponse('Not found') },
202
- tags: [resource],
203
- },
204
- get: {
205
- operationId: operationIds.get,
206
- parameters: [createParameterReference('Id'), createParameterReference('Embed')],
232
+ const shape = [selectionParameter(false)];
233
+ const list = [selectionParameter(true)];
234
+ const key = { in: 'path', name: entity.primary, required: true, schema: baseField({ ...entity.fields[entity.primary], generated: undefined }) };
235
+ const errors = { 400: response('Invalid request', ref('Error')), 404: response('Not found', ref('Error')), 409: response('Conflict', ref('Error')) };
236
+ const make = (operationId, parameters, schema, mutation) => {
237
+ if (operations.has(operationId))
238
+ throw new Error(`OpenAPI operation collision: ${operationId}`);
239
+ operations.add(operationId);
240
+ return {
241
+ operationId,
242
+ tags: [entity.collection],
243
+ parameters,
244
+ ...(auth && mutation ? { security: [{ AuthBearer: [] }] } : {}),
245
+ ...(mutation?.hasBody ? { requestBody: { required: true, ...json(ref(`${name}${capitalize(mutation.mode)}`)) } } : {}),
207
246
  responses: {
208
- 200: createResponse('Successful response', createSchemaReference(componentName)),
209
- 400: createErrorResponse('Invalid query'),
210
- 404: createErrorResponse('Not found'),
247
+ [mutation?.status ?? 200]: response('Success', schema),
248
+ ...errors,
249
+ ...(auth && mutation ? { 401: response('Authentication required', ref('Error')), 403: response('Forbidden', ref('Error')) } : {}),
211
250
  },
212
- tags: [resource],
213
- },
214
- patch: {
215
- operationId: operationIds.update,
216
- parameters: [createParameterReference('Id')],
217
- requestBody: createRequestBody(`${componentName}Update`),
218
- responses: {
219
- 200: createResponse('Updated', createSchemaReference(componentName)),
220
- 400: createErrorResponse('Invalid request'),
221
- 404: createErrorResponse('Not found'),
222
- },
223
- tags: [resource],
224
- },
225
- put: {
226
- operationId: operationIds.replace,
227
- parameters: [createParameterReference('Id')],
228
- requestBody: createRequestBody(`${componentName}Create`),
229
- responses: {
230
- 200: createResponse('Replaced', createSchemaReference(componentName)),
231
- 400: createErrorResponse('Invalid request'),
232
- 404: createErrorResponse('Not found'),
233
- },
234
- tags: [resource],
235
- },
236
- },
237
- };
238
- };
239
- const validateGeneratedNames = (resources, componentNames, files) => {
240
- const schemaOwners = new Map([
241
- ['Error', 'встроенная схема ошибки'],
242
- ...(files
243
- ? [
244
- ['FileMetadata', 'встроенная схема метаданных файла'],
245
- ['FileUpdate', 'встроенная схема изменения файла'],
246
- ]
247
- : []),
248
- ]);
249
- resources.forEach((resource) => {
250
- const componentName = componentNames[resource];
251
- if (componentName === '') {
252
- throw new Error(`Не удалось сформировать имя OpenAPI-схемы для ресурса «${resource}». Укажите $schema.${resource}.name`);
251
+ };
252
+ };
253
+ const collectionPath = `/${entity.collection}`;
254
+ const itemPath = `${collectionPath}/{${entity.primary}}`;
255
+ paths[collectionPath] = { get: make(`${op}List`, list, page(entity, entity.root)) };
256
+ paths[itemPath] = { get: make(op, [key, ...shape], output(entity, entity.root)) };
257
+ for (const mutation of MUTATIONS) {
258
+ paths[mutation.hasKey ? itemPath : collectionPath][mutation.method.toLowerCase()] = make(`${op}${capitalize(mutation.mode)}`, mutation.hasKey ? [key, ...shape] : shape, output(entity, entity.root), mutation);
253
259
  }
254
- [componentName, `${componentName}Create`, `${componentName}Update`, `${componentName}Page`].forEach((schemaName) => {
255
- const owner = schemaOwners.get(schemaName);
256
- if (owner != null) {
257
- throw new Error(`Имя OpenAPI-схемы «${schemaName}» используется ресурсами «${owner}» и «${resource}». Укажите уникальный $schema.${resource}.name`);
258
- }
259
- schemaOwners.set(schemaName, resource);
260
+ }
261
+ const parameters = {};
262
+ if (files) {
263
+ for (const [name, schema] of Object.entries({ FileMetadata: FILE_METADATA_SCHEMA, FileUpdate: FILE_UPDATE_SCHEMA })) {
264
+ if (schemas[name])
265
+ throw new Error(`OpenAPI schema collision: ${name}`);
266
+ schemas[name] = schema;
267
+ }
268
+ Object.assign(parameters, {
269
+ FilePath: { in: 'path', name: 'path', required: true, schema: { type: 'string' } },
270
+ ContentDirectory: { in: 'header', name: FILE_HEADERS.directory.name, schema: { type: 'string' } },
271
+ ContentName: { in: 'header', name: FILE_HEADERS.name.name, required: true, schema: { type: 'string' } },
272
+ ContentOverride: { in: 'header', name: FILE_HEADERS.override.name, schema: { type: 'string', enum: ['false', 'true'], default: 'false' } },
260
273
  });
261
- });
262
- };
263
- const validateOperationIds = (paths) => {
264
- const operationOwners = new Map();
265
- Object.entries(paths).forEach(([path, pathItem]) => {
266
- Object.entries(pathItem).forEach(([method, operation]) => {
267
- if (!isObject(operation) || typeof operation.operationId !== 'string') {
268
- return;
269
- }
270
- const owner = operationOwners.get(operation.operationId);
271
- if (owner != null) {
272
- throw new Error(`operationId «${operation.operationId}» используется операциями «${owner}» и «${method.toUpperCase()} ${path}»`);
274
+ for (const [path, item] of Object.entries(createFilePaths())) {
275
+ if (paths[path])
276
+ throw new Error(`File path collision: ${path}`);
277
+ for (const operation of Object.values(item)) {
278
+ if (isObject(operation) && typeof operation.operationId === 'string') {
279
+ if (operations.has(operation.operationId))
280
+ throw new Error(`OpenAPI operation collision: ${operation.operationId}`);
281
+ operations.add(operation.operationId);
282
+ }
273
283
  }
274
- operationOwners.set(operation.operationId, `${method.toUpperCase()} ${path}`);
275
- });
276
- });
277
- };
278
- /** Builds an OpenAPI document without runtime server addresses. */
279
- export function buildOpenapiDocument(options) {
280
- const { database, files = false, maxPageSize = DEFAULT_MAX_PAGE_SIZE, schema: schemaConfig = {} } = options ?? {};
281
- if (typeof files !== 'boolean') {
282
- throw new Error('Ключ files должен содержать boolean');
283
- }
284
- validateDatabase(database);
285
- if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
286
- throw new Error('Максимальный размер страницы должен быть положительным целым числом');
284
+ paths[path] = item;
285
+ }
287
286
  }
288
- if (!isObject(schemaConfig)) {
289
- throw new Error('Схема базы данных должна содержать JSON-объект');
287
+ if (auth) {
288
+ for (const [name, schema] of Object.entries(AUTH_SCHEMAS)) {
289
+ if (schemas[name])
290
+ throw new Error(`OpenAPI schema collision: ${name}`);
291
+ schemas[name] = structuredClone(schema);
292
+ }
293
+ for (const [path, item] of Object.entries(authOpenapiPaths())) {
294
+ if (paths[path] || model.entities.some((entity) => path.startsWith(`/${entity.collection}/`)))
295
+ throw new Error(`Auth path collision: ${path}`);
296
+ for (const operation of Object.values(item)) {
297
+ const id = operation.operationId;
298
+ if (operations.has(id))
299
+ throw new Error(`OpenAPI operation collision: ${id}`);
300
+ operations.add(id);
301
+ }
302
+ paths[path] = item;
303
+ }
290
304
  }
291
- const resources = getResourceNames(database);
292
- const resourceConfigs = normalizeSchemaConfig(schemaConfig, resources);
293
- const componentNames = Object.fromEntries(resources.map((resource) => {
294
- const resourceConfig = resourceConfigs[resource];
295
- const componentName = resourceConfig.name ?? toPascalCase(singularize(resource));
296
- return [resource, componentName];
297
- }));
298
- validateGeneratedNames(resources, componentNames, files);
299
- const rawSchemas = Object.fromEntries(resources.map((resource) => {
300
- const resourceConfig = resourceConfigs[resource];
301
- const inferredSchema = database[resource].length === 0 ? { properties: { id: { type: 'string' } }, type: 'object' } : inferObjectSchema(database[resource]);
302
- const configuredSchema = mergeSchemaOverrides(inferredSchema, { properties: resourceConfig.properties });
303
- return [resource, applyConfiguredFields(ensureGeneratedIdSchema(configuredSchema), resource, resourceConfig)];
304
- }));
305
- const schemas = {
306
- Error: { properties: { error: { type: 'string' } }, required: ['error'], type: 'object' },
307
- ...(files && {
308
- FileMetadata: FILE_METADATA_SCHEMA,
309
- FileUpdate: FILE_UPDATE_SCHEMA,
310
- }),
311
- };
312
- resources.forEach((resource) => {
313
- const componentName = componentNames[resource];
314
- const rawSchema = rawSchemas[resource];
315
- schemas[componentName] = addForwardRelations(rawSchema, resources, componentNames, resource);
316
- schemas[`${componentName}Create`] = omitId(rawSchema, true);
317
- schemas[`${componentName}Update`] = omitId(rawSchema, false);
318
- schemas[`${componentName}Page`] = {
319
- properties: {
320
- data: { items: createSchemaReference(componentName), type: 'array' },
321
- total: { minimum: 0, type: 'integer' },
322
- },
323
- required: ['data', 'total'],
324
- type: 'object',
325
- };
326
- });
327
- addReverseRelations(schemas, rawSchemas, resources, componentNames);
328
- const paths = Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource])), files ? createFilePaths() : {});
329
- validateOperationIds(paths);
330
- return {
331
- components: { parameters: createParameters(maxPageSize), schemas },
332
- info: isObject(schemaConfig.$info) ? schemaConfig.$info : { title: 'Deep JSON Server API', version: '1.0.0' },
333
- openapi: '3.0.3',
334
- paths,
335
- tags: [...new Set([...resources, ...(files ? ['files'] : [])])].map((name) => ({ name })),
336
- };
305
+ return { openapi: '3.0.3', info, components: { schemas, parameters, ...(auth ? { securitySchemes: structuredClone(AUTH_SECURITY_SCHEMES) } : {}) }, paths };
337
306
  }
338
307
  //# sourceMappingURL=document.js.map