@appweaver/core 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cache/eviction/lfu-eviction-index.js +3 -0
- package/export/export-service.d.ts +4 -2
- package/export/export-service.js +27 -17
- package/factory/create-model.js +72 -36
- package/factory/create-service.js +1 -1
- package/package.json +1 -1
- package/resource/index.d.ts +1 -0
- package/resource/index.js +1 -0
- package/resource/resource-schema.d.ts +0 -5
- package/resource/resource-schema.js +25 -10
- package/resource/resource-service.d.ts +214 -36
- package/resource/resource-service.js +247 -404
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +54 -0
- package/resource/schemas/resource-aggregate-schema.js +131 -0
- package/resource/schemas/resource-filter-schema.d.ts +39 -0
- package/resource/schemas/resource-filter-schema.js +153 -0
- package/resource/schemas/resource-sort-schema.d.ts +37 -0
- package/resource/schemas/resource-sort-schema.js +114 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +323 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +283 -0
- package/resource/utils/index.d.ts +4 -0
- package/resource/utils/index.js +20 -0
- package/resource/utils/relation-util.d.ts +84 -0
- package/resource/utils/relation-util.js +344 -0
- package/resource/utils/sort-util.d.ts +20 -0
- package/resource/utils/sort-util.js +218 -0
- package/security/create-auth-resources.js +2 -2
- package/security/helper.js +1 -1
- package/security/resources/api-key/model.js +1 -0
- package/security/resources/api-key/service.d.ts +1 -1
- package/security/resources/role/model.js +1 -1
- package/server/create-server.js +4 -1
- package/server/register-route.js +1 -0
- package/server/swagger.js +87 -21
- package/server/virtual-projection.js +10 -8
- package/types/generated.d.ts +110 -4
- package/utils/schema-util.js +1 -1
- package/utils/virtual-util.js +1 -1
package/server/swagger.js
CHANGED
|
@@ -11,7 +11,7 @@ const context_1 = require("../context");
|
|
|
11
11
|
exports.default = (0, fastify_plugin_1.default)((server) => {
|
|
12
12
|
server.register(swagger_1.default, {
|
|
13
13
|
hideUntagged: common_1.config.SWAGGER_HIDE_UNTAGGED,
|
|
14
|
-
transformObject: (document) => addConfig(pruneUnusedSchemas(document)),
|
|
14
|
+
transformObject: (document) => addConfig(normalizeUnionTypes(pruneUnusedSchemas(document))),
|
|
15
15
|
openapi: {
|
|
16
16
|
info: {
|
|
17
17
|
title: common_1.config.APP_NAME,
|
|
@@ -64,21 +64,22 @@ exports.default = (0, fastify_plugin_1.default)((server) => {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Rewrites the JSON Schema type lists of the document into the equivalent
|
|
69
|
+
* `anyOf` unions. The query filter schemas declare their plain values as a
|
|
70
|
+
* type list, so the request validator leaves the matching values untouched
|
|
71
|
+
* instead of coercing them into the first branch of a typed union, but a type
|
|
72
|
+
* list is only valid from OpenAPI 3.1 onwards, while the emitted document is
|
|
73
|
+
* an OpenAPI 3.0 one.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} document The OpenAPI document to rewrite in place.
|
|
76
|
+
* @returns {Object} The same document with every type list replaced by an
|
|
77
|
+
* `anyOf` union of the single-type schemas it listed.
|
|
78
|
+
*/
|
|
79
|
+
function normalizeUnionTypes(document) {
|
|
70
80
|
const visit = (node) => {
|
|
71
|
-
if (!node || typeof node !== 'object')
|
|
81
|
+
if (!node || typeof node !== 'object') {
|
|
72
82
|
return;
|
|
73
|
-
if (typeof node.$ref === 'string') {
|
|
74
|
-
const match = node.$ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
75
|
-
if (match) {
|
|
76
|
-
const schemaName = match[1];
|
|
77
|
-
if (!used.has(schemaName)) {
|
|
78
|
-
used.add(schemaName);
|
|
79
|
-
visit(schemas[schemaName]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
83
|
}
|
|
83
84
|
if (Array.isArray(node)) {
|
|
84
85
|
for (const item of node)
|
|
@@ -88,15 +89,31 @@ function pruneUnusedSchemas(document) {
|
|
|
88
89
|
for (const value of Object.values(node)) {
|
|
89
90
|
visit(value);
|
|
90
91
|
}
|
|
92
|
+
if (Array.isArray(node.type)) {
|
|
93
|
+
const branches = node.type.map((type) => ({ type }));
|
|
94
|
+
delete node.type;
|
|
95
|
+
// A node carrying both keywords keeps its own union as one branch, so
|
|
96
|
+
// the two constraints still have to be satisfied together
|
|
97
|
+
node.anyOf = node.anyOf ? [{ anyOf: node.anyOf }, ...branches] : branches;
|
|
98
|
+
}
|
|
91
99
|
};
|
|
92
|
-
visit(document.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.sort(([nameA, objA], [nameB, objB]) => String(objA.title ?? nameA).localeCompare(String(objB.title ?? nameB)))
|
|
96
|
-
.filter(([name]) => used.has(name)));
|
|
97
|
-
}
|
|
98
|
-
return document.openapiObject;
|
|
100
|
+
visit(document.paths);
|
|
101
|
+
visit(document.components?.schemas);
|
|
102
|
+
return document;
|
|
99
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Adds the `x-{CONFIG_NAME}-config` extension to the document, describing where
|
|
106
|
+
* the routes of the application are mounted. The generated client reads it to
|
|
107
|
+
* classify the paths of the specification into their route groups, which the
|
|
108
|
+
* paths alone do not reveal once the prefixes are configurable. It holds the
|
|
109
|
+
* base path of every resource route keyed by its model name, together with the
|
|
110
|
+
* configured prefixes of the api, static, health, auth, account, and file
|
|
111
|
+
* routes. Every path is normalized to start with a slash and to end without
|
|
112
|
+
* one.
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} document The OpenAPI document to extend in place.
|
|
115
|
+
* @returns {Object} The same document, carrying the configuration extension.
|
|
116
|
+
*/
|
|
100
117
|
function addConfig(document) {
|
|
101
118
|
const normalizePath = (path) => {
|
|
102
119
|
const withLeadingSlash = path.startsWith('/') ? path : `/${path}`;
|
|
@@ -120,3 +137,52 @@ function addConfig(document) {
|
|
|
120
137
|
};
|
|
121
138
|
return document;
|
|
122
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Removes the schemas no route refers to from the document and sorts the ones
|
|
142
|
+
* that remain by name. Every model registered on the server is added to the
|
|
143
|
+
* schema registry, including the variants no route uses, so the document is
|
|
144
|
+
* pruned down to the schemas actually reachable from a path. The reachable set
|
|
145
|
+
* is collected by walking the paths for `#/components/schemas/` references and
|
|
146
|
+
* following each one into the schema it points at, so the schemas referenced
|
|
147
|
+
* only by another schema (i.e. a nested relation model, or a query filter
|
|
148
|
+
* referring to itself) are kept as well.
|
|
149
|
+
*
|
|
150
|
+
* @param {Object} document The transform argument of the Swagger plugin,
|
|
151
|
+
* wrapping the OpenAPI document in its `openapiObject` property.
|
|
152
|
+
* @returns {Object} The unwrapped OpenAPI document, with the unreferenced
|
|
153
|
+
* schemas removed and the remaining ones ordered by their title, falling back
|
|
154
|
+
* to the name they are registered under.
|
|
155
|
+
*/
|
|
156
|
+
function pruneUnusedSchemas(document) {
|
|
157
|
+
const schemas = document.openapiObject.components?.schemas ?? {};
|
|
158
|
+
const used = new Set();
|
|
159
|
+
const visit = (node) => {
|
|
160
|
+
if (!node || typeof node !== 'object')
|
|
161
|
+
return;
|
|
162
|
+
if (typeof node.$ref === 'string') {
|
|
163
|
+
const match = node.$ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
164
|
+
if (match) {
|
|
165
|
+
const schemaName = match[1];
|
|
166
|
+
if (!used.has(schemaName)) {
|
|
167
|
+
used.add(schemaName);
|
|
168
|
+
visit(schemas[schemaName]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(node)) {
|
|
173
|
+
for (const item of node)
|
|
174
|
+
visit(item);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const value of Object.values(node)) {
|
|
178
|
+
visit(value);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
visit(document.openapiObject.paths);
|
|
182
|
+
if (document.openapiObject.components?.schemas) {
|
|
183
|
+
document.openapiObject.components.schemas = Object.fromEntries(Object.entries(document.openapiObject.components.schemas)
|
|
184
|
+
.sort(([nameA, objA], [nameB, objB]) => String(objA.title ?? nameA).localeCompare(String(objB.title ?? nameB)))
|
|
185
|
+
.filter(([name]) => used.has(name)));
|
|
186
|
+
}
|
|
187
|
+
return document.openapiObject;
|
|
188
|
+
}
|
|
@@ -47,7 +47,7 @@ function buildVirtualProjectionPlan(responseSchemas, getSchema) {
|
|
|
47
47
|
function createVirtualProjectionHook(plan) {
|
|
48
48
|
return async (_request, reply, payload) => {
|
|
49
49
|
const entries = plan[String(reply.statusCode)] ?? plan['2xx'];
|
|
50
|
-
if (!entries?.length || (!(0, common_1.
|
|
50
|
+
if (!entries?.length || (!(0, common_1.isPlainObject)(payload) && !(0, common_1.isArray)(payload))) {
|
|
51
51
|
return payload;
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
@@ -77,7 +77,7 @@ function createVirtualProjectionHook(plan) {
|
|
|
77
77
|
* @return {void}
|
|
78
78
|
*/
|
|
79
79
|
function collectProjectionEntries(schema, path, entries, getSchema, visitedRefs) {
|
|
80
|
-
if (!(0, common_1.
|
|
80
|
+
if (!(0, common_1.isPlainObject)(schema)) {
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
if ((0, common_1.isString)(schema['$ref'])) {
|
|
@@ -97,7 +97,7 @@ function collectProjectionEntries(schema, path, entries, getSchema, visitedRefs)
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
// OpenAPI-style response objects wrap the schema in media type content
|
|
100
|
-
if ((0, common_1.
|
|
100
|
+
if ((0, common_1.isPlainObject)(schema['content'])) {
|
|
101
101
|
for (const media of Object.values(schema['content'])) {
|
|
102
102
|
collectProjectionEntries(media?.schema, path, entries, getSchema, visitedRefs);
|
|
103
103
|
}
|
|
@@ -110,10 +110,10 @@ function collectProjectionEntries(schema, path, entries, getSchema, visitedRefs)
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
if ((0, common_1.
|
|
113
|
+
if ((0, common_1.isPlainObject)(schema['items'])) {
|
|
114
114
|
collectProjectionEntries(schema['items'], [...path, ARRAY_SEGMENT], entries, getSchema, visitedRefs);
|
|
115
115
|
}
|
|
116
|
-
if ((0, common_1.
|
|
116
|
+
if ((0, common_1.isPlainObject)(schema['properties'])) {
|
|
117
117
|
for (const [key, property] of Object.entries(schema['properties'])) {
|
|
118
118
|
collectProjectionEntries(property, [...path, key], entries, getSchema, visitedRefs);
|
|
119
119
|
}
|
|
@@ -213,9 +213,11 @@ function applyProjection(value, path, index, resourceName) {
|
|
|
213
213
|
}
|
|
214
214
|
if (index === path.length) {
|
|
215
215
|
if ((0, common_1.isArray)(value)) {
|
|
216
|
-
return value.map((item) => (0, common_1.
|
|
216
|
+
return value.map((item) => (0, common_1.isPlainObject)(item) ? (0, utils_1.projectVirtualFields)(item, resourceName) : item);
|
|
217
217
|
}
|
|
218
|
-
return (0, common_1.
|
|
218
|
+
return (0, common_1.isPlainObject)(value)
|
|
219
|
+
? (0, utils_1.projectVirtualFields)(value, resourceName)
|
|
220
|
+
: value;
|
|
219
221
|
}
|
|
220
222
|
const segment = path[index];
|
|
221
223
|
if (segment === ARRAY_SEGMENT) {
|
|
@@ -223,7 +225,7 @@ function applyProjection(value, path, index, resourceName) {
|
|
|
223
225
|
? value.map((item) => applyProjection(item, path, index + 1, resourceName))
|
|
224
226
|
: value;
|
|
225
227
|
}
|
|
226
|
-
if (!(0, common_1.
|
|
228
|
+
if (!(0, common_1.isPlainObject)(value) || value[segment] === undefined) {
|
|
227
229
|
return value;
|
|
228
230
|
}
|
|
229
231
|
value[segment] = applyProjection(value[segment], path, index + 1, resourceName);
|
package/types/generated.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AggregateSelect, QueryFilter, QuerySort } from '@appweaver/common';
|
|
1
2
|
export type ApiKey = {
|
|
2
3
|
id: number;
|
|
3
4
|
key: string;
|
|
@@ -43,6 +44,28 @@ export type ApiKeyUpdate = {
|
|
|
43
44
|
description?: string | null;
|
|
44
45
|
enabled?: boolean;
|
|
45
46
|
};
|
|
47
|
+
export type ApiKeyRelationCreate = {
|
|
48
|
+
name?: string | null;
|
|
49
|
+
description?: string | null;
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
expiresAt?: Date | null;
|
|
52
|
+
};
|
|
53
|
+
export type ApiKeyRelationUpdate = {
|
|
54
|
+
id: number;
|
|
55
|
+
name?: string | null;
|
|
56
|
+
description?: string | null;
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
};
|
|
59
|
+
export type ApiKeyRelationInput = {
|
|
60
|
+
id?: number;
|
|
61
|
+
name?: string | null;
|
|
62
|
+
description?: string | null;
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
expiresAt?: Date | null;
|
|
65
|
+
};
|
|
66
|
+
export type ApiKeyQuery = QueryFilter<ApiKey>;
|
|
67
|
+
export type ApiKeySort = QuerySort<ApiKeyMultiple>;
|
|
68
|
+
export type ApiKeyAggregate = AggregateSelect<ApiKey>;
|
|
46
69
|
export type OneTimeToken = {
|
|
47
70
|
id: number;
|
|
48
71
|
tokenHash: string;
|
|
@@ -82,6 +105,29 @@ export type OneTimeTokenUpdate = {
|
|
|
82
105
|
expiresAt?: Date;
|
|
83
106
|
data?: any;
|
|
84
107
|
};
|
|
108
|
+
export type OneTimeTokenRelationCreate = {
|
|
109
|
+
tokenHash: string;
|
|
110
|
+
purpose: string;
|
|
111
|
+
expiresAt: Date;
|
|
112
|
+
data: any;
|
|
113
|
+
};
|
|
114
|
+
export type OneTimeTokenRelationUpdate = {
|
|
115
|
+
id: number;
|
|
116
|
+
tokenHash?: string;
|
|
117
|
+
purpose?: string;
|
|
118
|
+
expiresAt?: Date;
|
|
119
|
+
data?: any;
|
|
120
|
+
};
|
|
121
|
+
export type OneTimeTokenRelationInput = {
|
|
122
|
+
id?: number;
|
|
123
|
+
tokenHash?: string;
|
|
124
|
+
purpose?: string;
|
|
125
|
+
expiresAt?: Date;
|
|
126
|
+
data?: any;
|
|
127
|
+
};
|
|
128
|
+
export type OneTimeTokenQuery = QueryFilter<OneTimeToken>;
|
|
129
|
+
export type OneTimeTokenSort = QuerySort<OneTimeTokenMultiple>;
|
|
130
|
+
export type OneTimeTokenAggregate = AggregateSelect<OneTimeToken>;
|
|
85
131
|
export type Permission = {
|
|
86
132
|
id: number;
|
|
87
133
|
name: string;
|
|
@@ -103,6 +149,20 @@ export type PermissionCreate = {
|
|
|
103
149
|
export type PermissionUpdate = {
|
|
104
150
|
name?: string;
|
|
105
151
|
};
|
|
152
|
+
export type PermissionRelationCreate = {
|
|
153
|
+
name: string;
|
|
154
|
+
};
|
|
155
|
+
export type PermissionRelationUpdate = {
|
|
156
|
+
id: number;
|
|
157
|
+
name?: string;
|
|
158
|
+
};
|
|
159
|
+
export type PermissionRelationInput = {
|
|
160
|
+
id?: number;
|
|
161
|
+
name?: string;
|
|
162
|
+
};
|
|
163
|
+
export type PermissionQuery = QueryFilter<Permission>;
|
|
164
|
+
export type PermissionSort = QuerySort<PermissionMultiple>;
|
|
165
|
+
export type PermissionAggregate = AggregateSelect<Permission>;
|
|
106
166
|
export type Role = {
|
|
107
167
|
id: number;
|
|
108
168
|
name: string;
|
|
@@ -114,25 +174,39 @@ export type Role = {
|
|
|
114
174
|
export type RoleSingle = {
|
|
115
175
|
id: number;
|
|
116
176
|
name: string;
|
|
117
|
-
permissions
|
|
177
|
+
permissions?: Array<PermissionSingle>;
|
|
118
178
|
};
|
|
119
179
|
export type RoleMultiple = {
|
|
120
180
|
id: number;
|
|
121
181
|
name: string;
|
|
122
|
-
permissions
|
|
182
|
+
permissions?: Array<PermissionSingle>;
|
|
123
183
|
};
|
|
124
184
|
export type RoleCreate = {
|
|
125
185
|
name: string;
|
|
126
186
|
permissions: Array<{
|
|
127
187
|
id: number;
|
|
128
|
-
}
|
|
188
|
+
} | number>;
|
|
129
189
|
};
|
|
130
190
|
export type RoleUpdate = {
|
|
131
191
|
name?: string;
|
|
132
192
|
permissions?: Array<{
|
|
133
193
|
id: number;
|
|
134
|
-
}
|
|
194
|
+
} | number>;
|
|
195
|
+
};
|
|
196
|
+
export type RoleRelationCreate = {
|
|
197
|
+
name: string;
|
|
198
|
+
};
|
|
199
|
+
export type RoleRelationUpdate = {
|
|
200
|
+
id: number;
|
|
201
|
+
name?: string;
|
|
135
202
|
};
|
|
203
|
+
export type RoleRelationInput = {
|
|
204
|
+
id?: number;
|
|
205
|
+
name?: string;
|
|
206
|
+
};
|
|
207
|
+
export type RoleQuery = QueryFilter<Role>;
|
|
208
|
+
export type RoleSort = QuerySort<RoleMultiple>;
|
|
209
|
+
export type RoleAggregate = AggregateSelect<Role>;
|
|
136
210
|
export type File = {
|
|
137
211
|
id: number;
|
|
138
212
|
name: string;
|
|
@@ -196,3 +270,35 @@ export type FileUpdate = {
|
|
|
196
270
|
title?: string | null;
|
|
197
271
|
description?: string | null;
|
|
198
272
|
};
|
|
273
|
+
export type FileRelationCreate = {
|
|
274
|
+
name: string;
|
|
275
|
+
originalName: string;
|
|
276
|
+
mimeType: string;
|
|
277
|
+
sizeBytes: number;
|
|
278
|
+
checksum: string;
|
|
279
|
+
title?: string | null;
|
|
280
|
+
description?: string | null;
|
|
281
|
+
};
|
|
282
|
+
export type FileRelationUpdate = {
|
|
283
|
+
id: number;
|
|
284
|
+
name?: string;
|
|
285
|
+
originalName?: string;
|
|
286
|
+
mimeType?: string;
|
|
287
|
+
sizeBytes?: number;
|
|
288
|
+
checksum?: string;
|
|
289
|
+
title?: string | null;
|
|
290
|
+
description?: string | null;
|
|
291
|
+
};
|
|
292
|
+
export type FileRelationInput = {
|
|
293
|
+
id?: number;
|
|
294
|
+
name?: string;
|
|
295
|
+
originalName?: string;
|
|
296
|
+
mimeType?: string;
|
|
297
|
+
sizeBytes?: number;
|
|
298
|
+
checksum?: string;
|
|
299
|
+
title?: string | null;
|
|
300
|
+
description?: string | null;
|
|
301
|
+
};
|
|
302
|
+
export type FileQuery = QueryFilter<File>;
|
|
303
|
+
export type FileSort = QuerySort<FileMultiple>;
|
|
304
|
+
export type FileAggregate = AggregateSelect<File>;
|
package/utils/schema-util.js
CHANGED
|
@@ -25,7 +25,7 @@ function createSchemaModel(schema, config = {}) {
|
|
|
25
25
|
schema.title ??
|
|
26
26
|
`Object${existingModels.length}`;
|
|
27
27
|
const reference = typebox_1.Type.Ref(name);
|
|
28
|
-
if (existingModels.find((m) => m.name === name) &&
|
|
28
|
+
if (existingModels.find((m) => m.value?.name === name) &&
|
|
29
29
|
config.skipExisting !== false) {
|
|
30
30
|
return reference;
|
|
31
31
|
}
|
package/utils/virtual-util.js
CHANGED
|
@@ -40,7 +40,7 @@ function projectVirtualFields(resource, resourceName) {
|
|
|
40
40
|
const value = projectedVirtual[key];
|
|
41
41
|
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
42
42
|
const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
|
|
43
|
-
if ((0, common_1.
|
|
43
|
+
if ((0, common_1.isPlainObject)(value) || ((0, common_1.isArray)(value) && (0, common_1.isPlainObject)(value[0]))) {
|
|
44
44
|
const nestedResourceName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
|
|
45
45
|
if (nestedResourceName) {
|
|
46
46
|
projectedVirtual[key] = (0, common_1.isArray)(value)
|