@appweaver/core 1.3.1 → 1.4.1
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/export/export-service.d.ts +4 -3
- package/export/export-service.js +39 -22
- package/factory/create-model.js +54 -22
- package/factory/create-service.js +5 -3
- package/package.json +2 -2
- package/prisma/client/commonInputTypes.d.ts +0 -50
- package/prisma/client/internal/class.js +3 -3
- package/prisma/client/models/File.d.ts +15 -28
- package/resource/resource-loader.js +6 -0
- package/resource/resource-routes.js +2 -2
- package/resource/resource-schema.d.ts +38 -12
- package/resource/resource-schema.js +63 -15
- package/resource/resource-service.d.ts +25 -16
- package/resource/resource-service.js +61 -26
- package/resource/schemas/resource-sort-schema.js +2 -2
- package/resource/utils/cursor-util.d.ts +60 -0
- package/resource/utils/cursor-util.js +117 -0
- package/resource/utils/index.d.ts +1 -0
- package/resource/utils/index.js +1 -0
- package/resource/utils/relation-util.d.ts +3 -3
- package/resource/utils/relation-util.js +52 -16
- package/resource/utils/sort-util.d.ts +11 -0
- package/resource/utils/sort-util.js +18 -0
- package/security/api-key/api-key-auth.js +4 -1
- package/security/auth-service.d.ts +7 -7
- package/security/auth-service.js +43 -5
- package/security/create-auth-resources.d.ts +2 -1
- package/security/oauth2/create-oauth2-plugin.js +3 -34
- package/security/oauth2/oauth2-microsoft.js +1 -1
- package/security/oauth2/oauth2-util.d.ts +9 -7
- package/security/oauth2/oauth2-util.js +17 -10
- package/security/resources/api-key/model.js +2 -0
- package/security/resources/connected-account/model.js +2 -0
- package/security/store/database-security-store.js +2 -1
- package/server/swagger.js +49 -1
- package/storage/file-service.d.ts +19 -4
- package/storage/file-service.js +199 -146
- package/storage/resources/file/model.js +4 -2
- package/types/auth.d.ts +8 -8
- package/types/generated.d.ts +8 -2
- package/types/index.d.ts +1 -0
- package/types/index.js +1 -0
- package/types/storage.d.ts +16 -0
- package/types/storage.js +2 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/model-util.d.ts +12 -0
- package/utils/model-util.js +113 -0
|
@@ -48,7 +48,7 @@ class ResourceService {
|
|
|
48
48
|
* configured for output on the find action. A resource event is emitted after
|
|
49
49
|
* a successful lookup.
|
|
50
50
|
*
|
|
51
|
-
* @param {
|
|
51
|
+
* @param {ResourceId} id The id of the resource to find.
|
|
52
52
|
* @returns {Promise<Object>} The found resource with its virtual fields and
|
|
53
53
|
* relation counts projected.
|
|
54
54
|
* @throws {@link HttpError} 404 if the resource does not exist or is filtered
|
|
@@ -95,52 +95,85 @@ class ResourceService {
|
|
|
95
95
|
* as plain field values. Its `searchText` property, if present, is passed to
|
|
96
96
|
* {@link ResourceService.textSearchQuery} instead of being matched as a
|
|
97
97
|
* field.
|
|
98
|
-
* @param {number} [page] The one-based page number of results to return
|
|
98
|
+
* @param {number} [page] The one-based page number of results to return,
|
|
99
|
+
* ignored when a cursor is given.
|
|
99
100
|
* @param {number} [size] The maximum number of results per page.
|
|
100
101
|
* @param {QuerySort} [sort] The fields to sort by, either as a comma-separated
|
|
101
102
|
* list where a field prefixed with `-` is sorted in descending order
|
|
102
|
-
* (i.e. `-createdAt,
|
|
103
|
+
* (i.e. `-createdAt,title`), or as an object of field directions
|
|
103
104
|
* (i.e. `{ createdAt: 'desc', id: 'asc' }`). Both forms support the fields of
|
|
104
105
|
* the included to-one relations, given with a dot notation (`author.createdAt`)
|
|
105
106
|
* or as a nested object (`{ author: { createdAt: 'desc' } }`).
|
|
107
|
+
* @param {string} [cursor] The cursor of the page to return, as issued in the
|
|
108
|
+
* `nextCursor` or `prevCursor` of an earlier response, which also carries the
|
|
109
|
+
* direction the page runs in. Takes precedence over `page`.
|
|
110
|
+
* @param {boolean} [totalCount] Whether to count all matching resources, which
|
|
111
|
+
* costs a scan of every one of them.
|
|
106
112
|
* @returns {Promise<QueryResponse<Object>>} The paged query response containing
|
|
107
|
-
* the returned resources, the count of the returned items
|
|
108
|
-
*
|
|
113
|
+
* the returned resources, the count of the returned items, the cursors of the
|
|
114
|
+
* adjacent pages, and the total count unless it was opted out of.
|
|
109
115
|
* @throws {@link HttpError} 400 if the sort input names a field that cannot be
|
|
110
|
-
* sorted by
|
|
116
|
+
* sorted by or the cursor was issued for another filter or sort order, and 500
|
|
117
|
+
* on a database error.
|
|
111
118
|
*/
|
|
112
|
-
async query(filter = {}, page = 1, size = 50, sort = '-createdAt,
|
|
119
|
+
async query(filter = {}, page = 1, size = 50, sort = '-createdAt', cursor, totalCount = true) {
|
|
113
120
|
const restrictions = await this.readRestrictions('query', filter);
|
|
114
121
|
const textSearch = this.extractTextSearchQuery(filter);
|
|
115
122
|
const mappedFilter = (0, utils_2.mapQueryFilter)(filter, this._client.name);
|
|
116
123
|
const query = { AND: [mappedFilter, textSearch, restrictions] };
|
|
117
124
|
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'query');
|
|
118
|
-
const orderBy = (0, utils_2.
|
|
125
|
+
const orderBy = (0, utils_2.mapStableSortValues)(sort, this._client.name, 'query');
|
|
126
|
+
// Binding to the mapped query rather than the filter also invalidates a
|
|
127
|
+
// cursor on a changed text search or read restriction
|
|
128
|
+
const fingerprint = (0, utils_2.queryFingerprint)(this._client.name, query, orderBy);
|
|
129
|
+
const decodedCursor = (0, utils_2.decodeCursor)(cursor, fingerprint);
|
|
130
|
+
// One record past the page tells whether a further page exists
|
|
131
|
+
const backward = decodedCursor?.backward === true;
|
|
132
|
+
const take = size > 0 ? size + 1 : 0;
|
|
133
|
+
const findMany = this._client.findMany({
|
|
134
|
+
where: { ...query },
|
|
135
|
+
include: includeRelations,
|
|
136
|
+
// A cursor skips the record it addresses, an offset the pages before it
|
|
137
|
+
cursor: decodedCursor ? { id: decodedCursor.id } : undefined,
|
|
138
|
+
skip: decodedCursor ? 1 : (page - 1) * size,
|
|
139
|
+
take: backward ? -take : take,
|
|
140
|
+
orderBy
|
|
141
|
+
});
|
|
119
142
|
let resources;
|
|
120
|
-
let
|
|
143
|
+
let count;
|
|
121
144
|
try {
|
|
122
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
]);
|
|
145
|
+
if (totalCount) {
|
|
146
|
+
[resources, count] = await this._db
|
|
147
|
+
.client()
|
|
148
|
+
.$transaction([
|
|
149
|
+
findMany,
|
|
150
|
+
this._client.count({ where: { ...query } })
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
resources = await findMany;
|
|
155
|
+
}
|
|
134
156
|
}
|
|
135
157
|
catch (e) {
|
|
136
158
|
throw new errors_1.HttpError(`${this._client.name} query error`, 500, e);
|
|
137
159
|
}
|
|
160
|
+
// The over-fetched record leads a backward page and trails a forward one
|
|
161
|
+
const hasMore = resources.length > size;
|
|
162
|
+
if (hasMore) {
|
|
163
|
+
resources = backward
|
|
164
|
+
? resources.slice(resources.length - size)
|
|
165
|
+
: resources.slice(0, size);
|
|
166
|
+
}
|
|
167
|
+
// The page a cursor was followed from always exists
|
|
168
|
+
const hasNext = backward || hasMore;
|
|
169
|
+
const hasPrev = backward ? hasMore : !!decodedCursor || page > 1;
|
|
138
170
|
this._events.emitResourceEvent(this._client.name, 'query', {
|
|
139
171
|
current: resources
|
|
140
172
|
});
|
|
141
173
|
return {
|
|
142
174
|
resultCount: resources.length,
|
|
143
|
-
totalCount,
|
|
175
|
+
totalCount: count ?? null,
|
|
176
|
+
...(0, utils_2.pageCursors)(resources, fingerprint, hasNext, hasPrev),
|
|
144
177
|
items: resources.map((resource) => this.projectResource(resource))
|
|
145
178
|
};
|
|
146
179
|
}
|
|
@@ -174,7 +207,9 @@ class ResourceService {
|
|
|
174
207
|
* When false, the step is interpreted in seconds.
|
|
175
208
|
* @returns {Promise<AggregateResponse<Object>>} The aggregation response with
|
|
176
209
|
* the overall total and one result per period, each labeled with the median
|
|
177
|
-
* date of its period.
|
|
210
|
+
* date of its period. It is typed by the fields the selection named, not by
|
|
211
|
+
* the whole model, whenever the selection is passed as an object literal or
|
|
212
|
+
* annotated with `satisfies`.
|
|
178
213
|
* @throws {@link HttpError} 400 if the selection is empty or names a field or
|
|
179
214
|
* operator that cannot be aggregated, and 500 on a database error.
|
|
180
215
|
*/
|
|
@@ -284,7 +319,7 @@ class ResourceService {
|
|
|
284
319
|
* and a resource event carrying both the previous and the current state is
|
|
285
320
|
* emitted after a successful update.
|
|
286
321
|
*
|
|
287
|
-
* @param {
|
|
322
|
+
* @param {ResourceId} id The id of the resource to update.
|
|
288
323
|
* @param {Object} data The partial data to update the resource with, including
|
|
289
324
|
* any inline relation and file payloads.
|
|
290
325
|
* @returns {Promise<Object>} The updated resource with its virtual fields and
|
|
@@ -355,7 +390,7 @@ class ResourceService {
|
|
|
355
390
|
* inside a single transaction. The resource cache is invalidated and a
|
|
356
391
|
* resource event is emitted after a successful delete.
|
|
357
392
|
*
|
|
358
|
-
* @param {
|
|
393
|
+
* @param {ResourceId} id The id of the resource to delete.
|
|
359
394
|
* @returns {Promise<Object>} The deleted resource with its virtual fields and
|
|
360
395
|
* relation counts projected.
|
|
361
396
|
* @throws {@link HttpError} 404 if the resource does not exist or is filtered
|
|
@@ -408,7 +443,7 @@ class ResourceService {
|
|
|
408
443
|
*
|
|
409
444
|
* @param {ActionType} action The called action method on this service (find,
|
|
410
445
|
* query, aggregate, update, or delete)
|
|
411
|
-
* @param {Object|
|
|
446
|
+
* @param {Object|ResourceId} data The passed data to the called function can be
|
|
412
447
|
* number or object. If the data is a type of number, then it represents the
|
|
413
448
|
* resource id, otherwise it depends on the action and can be one of the
|
|
414
449
|
* following:
|
|
@@ -101,10 +101,10 @@ function registerQuerySortSchemas() {
|
|
|
101
101
|
function querySortSchema(modelName) {
|
|
102
102
|
return typebox_1.Type.Optional(typebox_1.Type.Union([
|
|
103
103
|
typebox_1.Type.String({
|
|
104
|
-
example: '-createdAt,
|
|
104
|
+
example: '-createdAt,title',
|
|
105
105
|
description: 'Comma-separated list of fields to sort by, where a field prefixed ' +
|
|
106
106
|
'with `-` is sorted in descending order and a dot notation path ' +
|
|
107
|
-
'targets a field of an included relation
|
|
107
|
+
'targets a field of an included relation'
|
|
108
108
|
}),
|
|
109
109
|
typebox_1.Type.Ref((0, exports.querySortName)(modelName))
|
|
110
110
|
], {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { QueryResponse, ResourceId } from '@appweaver/common';
|
|
2
|
+
/** The cursors a query response carries, kept in step with the response contract. */
|
|
3
|
+
export type PageCursors = Pick<QueryResponse<unknown>, 'nextCursor' | 'prevCursor'>;
|
|
4
|
+
/** A decoded cursor, holding the record it points at and the direction to page in. */
|
|
5
|
+
export type DecodedCursor = {
|
|
6
|
+
id: ResourceId;
|
|
7
|
+
/** True when the cursor pages towards the preceding records */
|
|
8
|
+
backward: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds the fingerprint identifying the query a cursor belongs to. A cursor
|
|
12
|
+
* only yields the intended records while the resource, the query, and the order
|
|
13
|
+
* stay the same, so it carries the fingerprint and is rejected on mismatch.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} resourceName - The name of the queried model.
|
|
16
|
+
* @param {Object} query - The mapped database query the cursor was issued for.
|
|
17
|
+
* @param {Object[]} orderBy - The mapped order entries the cursor was issued for.
|
|
18
|
+
* @return {string} The fingerprint of the query.
|
|
19
|
+
*/
|
|
20
|
+
export declare function queryFingerprint(resourceName: string, query: any, orderBy: any[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Encodes the cursor of a page adjacent to a query result. The direction belongs
|
|
23
|
+
* to the cursor rather than to the request, so a caller cannot pair one with a
|
|
24
|
+
* direction it was not issued for.
|
|
25
|
+
*
|
|
26
|
+
* @param {ResourceId} id - The primary key of the record the page continues from.
|
|
27
|
+
* @param {string} fingerprint - The fingerprint of the query, as built by
|
|
28
|
+
* {@link queryFingerprint}.
|
|
29
|
+
* @param {boolean} [backward] - Whether the cursor pages towards the preceding
|
|
30
|
+
* records.
|
|
31
|
+
* @return {string} The encoded cursor.
|
|
32
|
+
*/
|
|
33
|
+
export declare function encodeCursor(id: ResourceId, fingerprint: string, backward?: boolean): string;
|
|
34
|
+
/**
|
|
35
|
+
* Builds the cursors of the pages adjacent to a returned page, each addressing
|
|
36
|
+
* the record of this page it continues from. A page that has no neighbour in a
|
|
37
|
+
* direction, and an empty page, yield null rather than an absent cursor.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object[]} resources - The records of the returned page, in the order
|
|
40
|
+
* they were queried in.
|
|
41
|
+
* @param {string} fingerprint - The fingerprint of the query the page belongs to.
|
|
42
|
+
* @param {boolean} hasNext - Whether a page follows the returned one.
|
|
43
|
+
* @param {boolean} hasPrev - Whether a page precedes the returned one.
|
|
44
|
+
* @return {PageCursors} The cursors of the existing adjacent pages.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pageCursors<T>(resources: T[], fingerprint: string, hasNext: boolean, hasPrev: boolean): PageCursors;
|
|
47
|
+
/**
|
|
48
|
+
* Decodes the cursor a query request carries, resolving the record the page
|
|
49
|
+
* continues from and the direction it runs in.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} [cursor] - The cursor of the request, which may be the null
|
|
52
|
+
* one of a page that does not exist.
|
|
53
|
+
* @param {string} fingerprint - The fingerprint of the query the cursor is used
|
|
54
|
+
* on, as built by {@link queryFingerprint}.
|
|
55
|
+
* @return {DecodedCursor | undefined} The decoded cursor, or `undefined` when the
|
|
56
|
+
* request carries none.
|
|
57
|
+
* @throws {HttpError} 400 if the cursor is malformed, or was issued for another
|
|
58
|
+
* resource, filter, or sort order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function decodeCursor(cursor: string | null | undefined, fingerprint: string): DecodedCursor | undefined;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryFingerprint = queryFingerprint;
|
|
4
|
+
exports.encodeCursor = encodeCursor;
|
|
5
|
+
exports.pageCursors = pageCursors;
|
|
6
|
+
exports.decodeCursor = decodeCursor;
|
|
7
|
+
const common_1 = require("@appweaver/common");
|
|
8
|
+
const errors_1 = require("../../errors");
|
|
9
|
+
/**
|
|
10
|
+
* Builds the fingerprint identifying the query a cursor belongs to. A cursor
|
|
11
|
+
* only yields the intended records while the resource, the query, and the order
|
|
12
|
+
* stay the same, so it carries the fingerprint and is rejected on mismatch.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} resourceName - The name of the queried model.
|
|
15
|
+
* @param {Object} query - The mapped database query the cursor was issued for.
|
|
16
|
+
* @param {Object[]} orderBy - The mapped order entries the cursor was issued for.
|
|
17
|
+
* @return {string} The fingerprint of the query.
|
|
18
|
+
*/
|
|
19
|
+
function queryFingerprint(resourceName, query, orderBy) {
|
|
20
|
+
const serialized = stableStringify([resourceName, query ?? {}, orderBy]);
|
|
21
|
+
return (0, common_1.makeHash)(serialized, 'sha256', 'base64url').slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Encodes the cursor of a page adjacent to a query result. The direction belongs
|
|
25
|
+
* to the cursor rather than to the request, so a caller cannot pair one with a
|
|
26
|
+
* direction it was not issued for.
|
|
27
|
+
*
|
|
28
|
+
* @param {ResourceId} id - The primary key of the record the page continues from.
|
|
29
|
+
* @param {string} fingerprint - The fingerprint of the query, as built by
|
|
30
|
+
* {@link queryFingerprint}.
|
|
31
|
+
* @param {boolean} [backward] - Whether the cursor pages towards the preceding
|
|
32
|
+
* records.
|
|
33
|
+
* @return {string} The encoded cursor.
|
|
34
|
+
*/
|
|
35
|
+
function encodeCursor(id, fingerprint, backward = false) {
|
|
36
|
+
const payload = { i: id, f: fingerprint };
|
|
37
|
+
if (backward) {
|
|
38
|
+
payload.b = true;
|
|
39
|
+
}
|
|
40
|
+
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds the cursors of the pages adjacent to a returned page, each addressing
|
|
44
|
+
* the record of this page it continues from. A page that has no neighbour in a
|
|
45
|
+
* direction, and an empty page, yield null rather than an absent cursor.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object[]} resources - The records of the returned page, in the order
|
|
48
|
+
* they were queried in.
|
|
49
|
+
* @param {string} fingerprint - The fingerprint of the query the page belongs to.
|
|
50
|
+
* @param {boolean} hasNext - Whether a page follows the returned one.
|
|
51
|
+
* @param {boolean} hasPrev - Whether a page precedes the returned one.
|
|
52
|
+
* @return {PageCursors} The cursors of the existing adjacent pages.
|
|
53
|
+
*/
|
|
54
|
+
function pageCursors(resources, fingerprint, hasNext, hasPrev) {
|
|
55
|
+
// A queried record always carries its primary key, which a model type need
|
|
56
|
+
// not declare
|
|
57
|
+
const first = resources[0];
|
|
58
|
+
const last = resources[resources.length - 1];
|
|
59
|
+
if (!first || !last) {
|
|
60
|
+
return { nextCursor: null, prevCursor: null };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
nextCursor: hasNext ? encodeCursor(last.id, fingerprint) : null,
|
|
64
|
+
prevCursor: hasPrev ? encodeCursor(first.id, fingerprint, true) : null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decodes the cursor a query request carries, resolving the record the page
|
|
69
|
+
* continues from and the direction it runs in.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} [cursor] - The cursor of the request, which may be the null
|
|
72
|
+
* one of a page that does not exist.
|
|
73
|
+
* @param {string} fingerprint - The fingerprint of the query the cursor is used
|
|
74
|
+
* on, as built by {@link queryFingerprint}.
|
|
75
|
+
* @return {DecodedCursor | undefined} The decoded cursor, or `undefined` when the
|
|
76
|
+
* request carries none.
|
|
77
|
+
* @throws {HttpError} 400 if the cursor is malformed, or was issued for another
|
|
78
|
+
* resource, filter, or sort order.
|
|
79
|
+
*/
|
|
80
|
+
function decodeCursor(cursor, fingerprint) {
|
|
81
|
+
if (!cursor) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
let payload;
|
|
85
|
+
try {
|
|
86
|
+
payload = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
throw new errors_1.HttpError('Invalid pagination cursor', 400, e);
|
|
90
|
+
}
|
|
91
|
+
if (!(0, common_1.isPlainObject)(payload) || payload.i === undefined) {
|
|
92
|
+
throw new errors_1.HttpError('Invalid pagination cursor', 400);
|
|
93
|
+
}
|
|
94
|
+
if (payload.f !== fingerprint) {
|
|
95
|
+
throw new errors_1.HttpError('Pagination cursor does not match the filter and sort of this query', 400);
|
|
96
|
+
}
|
|
97
|
+
return { id: payload.i, backward: payload.b === true };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Serializes a value with its object keys in a stable order, so two equal
|
|
101
|
+
* queries fingerprint identically whatever order their properties arrived in.
|
|
102
|
+
*
|
|
103
|
+
* @param {any} value - The value to serialize.
|
|
104
|
+
* @return {string} The serialized value.
|
|
105
|
+
*/
|
|
106
|
+
function stableStringify(value) {
|
|
107
|
+
if ((0, common_1.isArray)(value)) {
|
|
108
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
109
|
+
}
|
|
110
|
+
if ((0, common_1.isPlainObject)(value)) {
|
|
111
|
+
const entries = Object.keys(value)
|
|
112
|
+
.sort()
|
|
113
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
114
|
+
return `{${entries.join(',')}}`;
|
|
115
|
+
}
|
|
116
|
+
return JSON.stringify(value) ?? 'null';
|
|
117
|
+
}
|
package/resource/utils/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./aggregate-util"), exports);
|
|
18
|
+
__exportStar(require("./cursor-util"), exports);
|
|
18
19
|
__exportStar(require("./filter-util"), exports);
|
|
19
20
|
__exportStar(require("./relation-util"), exports);
|
|
20
21
|
__exportStar(require("./sort-util"), exports);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionType } from '@appweaver/common';
|
|
1
|
+
import { ActionType, ResourceId } from '@appweaver/common';
|
|
2
2
|
/** The nested write actions a single relation field can be mapped to. */
|
|
3
3
|
export type RelationActions = Record<string, Partial<{
|
|
4
4
|
connect: any;
|
|
@@ -74,11 +74,11 @@ export declare function missingRelationFields(resourceName: string | undefined,
|
|
|
74
74
|
* user. Returns undefined when the model does not audit the `createdById` field or when no user is authenticated.
|
|
75
75
|
*
|
|
76
76
|
* @param {string} resourceName - The name of the model the audit relation is built for.
|
|
77
|
-
* @return {{connect: {id:
|
|
77
|
+
* @return {{connect: {id: ResourceId}}|undefined} The connect action pointing at the id of the currently authenticated
|
|
78
78
|
* user, or undefined if the model does not audit the `createdById` field or no user is authenticated.
|
|
79
79
|
*/
|
|
80
80
|
export declare function createdByConnect(resourceName: string): {
|
|
81
81
|
connect: {
|
|
82
|
-
id:
|
|
82
|
+
id: ResourceId;
|
|
83
83
|
};
|
|
84
84
|
} | undefined;
|
|
@@ -9,6 +9,9 @@ const common_1 = require("@appweaver/common");
|
|
|
9
9
|
const context_1 = require("../../context");
|
|
10
10
|
const security_1 = require("../../security");
|
|
11
11
|
const errors_1 = require("../../errors");
|
|
12
|
+
/** Levels of a self referencing relation read when the relation configures no
|
|
13
|
+
* `maxDepth`, i.e. the relation itself and nothing below it. */
|
|
14
|
+
const DEFAULT_RELATION_MAX_DEPTH = 1;
|
|
12
15
|
/**
|
|
13
16
|
* Builds the Prisma `include` clause for the relation and file fields of a resource model. A field is included when
|
|
14
17
|
* its configured output type allows it for the given action, or always when no action is specified, and the relations
|
|
@@ -41,7 +44,7 @@ function mapRelationInclusions(resourceName, action) {
|
|
|
41
44
|
}
|
|
42
45
|
// Check if the relation should be included based on the output type
|
|
43
46
|
if (shouldIncludeRelation(relationField?.output?.type, action)) {
|
|
44
|
-
inclusion[key] = buildNestedInclusion(relationField, action);
|
|
47
|
+
inclusion[key] = buildNestedInclusion(relationField, key, action);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
return inclusion;
|
|
@@ -276,7 +279,7 @@ function missingRelationFields(resourceName, data) {
|
|
|
276
279
|
* user. Returns undefined when the model does not audit the `createdById` field or when no user is authenticated.
|
|
277
280
|
*
|
|
278
281
|
* @param {string} resourceName - The name of the model the audit relation is built for.
|
|
279
|
-
* @return {{connect: {id:
|
|
282
|
+
* @return {{connect: {id: ResourceId}}|undefined} The connect action pointing at the id of the currently authenticated
|
|
280
283
|
* user, or undefined if the model does not audit the `createdById` field or no user is authenticated.
|
|
281
284
|
*/
|
|
282
285
|
function createdByConnect(resourceName) {
|
|
@@ -294,34 +297,67 @@ function createdByConnect(resourceName) {
|
|
|
294
297
|
: undefined;
|
|
295
298
|
}
|
|
296
299
|
/**
|
|
297
|
-
* Resolves the inclusion value of a single relation field
|
|
298
|
-
*
|
|
299
|
-
* `include` clause otherwise.
|
|
300
|
+
* Resolves the inclusion value of a single relation field, walking both the levels a self referencing relation
|
|
301
|
+
* repeats itself for and the nested includes it configures. Returns `true` when the relation reads no further than
|
|
302
|
+
* itself for the given action, or a nested `include` clause otherwise.
|
|
303
|
+
*
|
|
304
|
+
* A relation whose related model holds the same relation again points back at its own model, so it repeats itself
|
|
305
|
+
* down the tree up to its configured `output.maxDepth`, letting a category carry its ancestors without every level
|
|
306
|
+
* being spelled out. An `output.include` entry naming that same relation configures the level itself and replaces
|
|
307
|
+
* the repetition.
|
|
300
308
|
*
|
|
301
309
|
* @param {RelationField} relationField - The configuration of the relation whose inclusion value is resolved, read
|
|
302
|
-
* from its `output.include`
|
|
310
|
+
* from its `output.include` and `output.maxDepth` properties.
|
|
311
|
+
* @param {string} key - The field name the relation is declared under, matched against the related model to detect a
|
|
312
|
+
* relation pointing back at its own model.
|
|
303
313
|
* @param {ActionType} [action] - The action the inclusions are built for, matched against the configured output type
|
|
304
314
|
* of every nested relation.
|
|
305
|
-
* @
|
|
306
|
-
* nested `include` clause
|
|
315
|
+
* @param {number} [depth=1] - The level of the relation being resolved, counting the relation itself as the first.
|
|
316
|
+
* @return {boolean|Object} True if the relation reads no further than itself, or the nested `include` clause
|
|
317
|
+
* otherwise.
|
|
307
318
|
*/
|
|
308
|
-
function buildNestedInclusion(relationField, action) {
|
|
319
|
+
function buildNestedInclusion(relationField, key, action, depth = 1) {
|
|
309
320
|
const nestedIncludeConfig = relationField?.output?.include;
|
|
310
|
-
if (!nestedIncludeConfig || Object.keys(nestedIncludeConfig).length === 0) {
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
321
|
const nestedInclusion = {};
|
|
314
|
-
|
|
322
|
+
// A file field carries no related model, so it reads no further than itself
|
|
323
|
+
const relatedModel = relationField?.model
|
|
324
|
+
? (0, context_1.injectModel)((0, common_1.capitalize)(relationField.model), false)
|
|
325
|
+
: undefined;
|
|
326
|
+
const selfRelation = selfReferencingRelation(relatedModel, key);
|
|
327
|
+
const maxDepth = relationField?.output?.maxDepth ?? DEFAULT_RELATION_MAX_DEPTH;
|
|
328
|
+
if (selfRelation && depth < maxDepth && !nestedIncludeConfig?.[key]) {
|
|
329
|
+
nestedInclusion[key] = buildNestedInclusion({ ...selfRelation, output: relationField.output }, key, action, depth + 1);
|
|
330
|
+
}
|
|
331
|
+
for (const [nestedKey, nestedOutput] of Object.entries(nestedIncludeConfig ?? {})) {
|
|
315
332
|
// Check if the nested relation should be included
|
|
316
|
-
if (shouldIncludeRelation(nestedOutput?.type, action)) {
|
|
317
|
-
|
|
318
|
-
nestedInclusion[nestedKey] = buildNestedInclusion({ output: nestedOutput }, action);
|
|
333
|
+
if (!shouldIncludeRelation(nestedOutput?.type, action)) {
|
|
334
|
+
continue;
|
|
319
335
|
}
|
|
336
|
+
// The related model carries the field the nested include names, so each
|
|
337
|
+
// level is resolved against the model it actually belongs to
|
|
338
|
+
const nestedRelation = relatedModel?.config?.relations?.[nestedKey] ??
|
|
339
|
+
relatedModel?.config?.files?.[nestedKey];
|
|
340
|
+
nestedInclusion[nestedKey] = buildNestedInclusion({ ...nestedRelation, output: nestedOutput }, nestedKey, action);
|
|
320
341
|
}
|
|
321
342
|
return Object.keys(nestedInclusion).length > 0
|
|
322
343
|
? { include: nestedInclusion }
|
|
323
344
|
: true;
|
|
324
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Reads the relation a related model holds under the given field name, when it points back at that same model. It is
|
|
348
|
+
* the relation a self referencing field repeats itself through, i.e. the `parent` of the parent of a category.
|
|
349
|
+
*
|
|
350
|
+
* @param {ResourceModel} [model] - The related model the field is looked up on.
|
|
351
|
+
* @param {string} key - The field name of the relation.
|
|
352
|
+
* @return {RelationField | undefined} The relation of the model under that name when it references the model itself,
|
|
353
|
+
* undefined otherwise.
|
|
354
|
+
*/
|
|
355
|
+
function selfReferencingRelation(model, key) {
|
|
356
|
+
const relation = key ? model?.config?.relations?.[key] : undefined;
|
|
357
|
+
return relation && (0, common_1.capitalize)(relation.model) === model?.name
|
|
358
|
+
? relation
|
|
359
|
+
: undefined;
|
|
360
|
+
}
|
|
325
361
|
/**
|
|
326
362
|
* Decides whether a relation with the given configured output type is included for the given action. The `none` type
|
|
327
363
|
* is never included, the `single` type is excluded from the query action, and the `multiple` type is only included on
|
|
@@ -18,3 +18,14 @@ import { ActionType, QuerySort } from '@appweaver/common';
|
|
|
18
18
|
* direction, or targets a relation that the action does not include in its response.
|
|
19
19
|
*/
|
|
20
20
|
export declare function mapSortValues(sort: QuerySort, resourceName: string, action?: ActionType): any[];
|
|
21
|
+
/**
|
|
22
|
+
* Maps a sort input the same way {@link mapSortValues} does, and terminates it with the primary key when no entry
|
|
23
|
+
* orders by it. Without a total order, records sharing every sort value can be skipped or repeated across pages.
|
|
24
|
+
*
|
|
25
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
26
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
27
|
+
* @param {ActionType} [action] - The action the sort is applied on. Defaults to the query action.
|
|
28
|
+
* @return {Object[]} The `orderBy` entries, ending in a unique one.
|
|
29
|
+
* @throws {HttpError} 400 under the same conditions as {@link mapSortValues}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mapStableSortValues(sort: QuerySort, resourceName: string, action?: ActionType): any[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.mapSortValues = mapSortValues;
|
|
4
|
+
exports.mapStableSortValues = mapStableSortValues;
|
|
4
5
|
const common_1 = require("@appweaver/common");
|
|
5
6
|
const context_1 = require("../../context");
|
|
6
7
|
const errors_1 = require("../../errors");
|
|
@@ -46,6 +47,23 @@ function mapSortValues(sort, resourceName, action = 'query') {
|
|
|
46
47
|
}
|
|
47
48
|
return orderBy;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Maps a sort input the same way {@link mapSortValues} does, and terminates it with the primary key when no entry
|
|
52
|
+
* orders by it. Without a total order, records sharing every sort value can be skipped or repeated across pages.
|
|
53
|
+
*
|
|
54
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
55
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
56
|
+
* @param {ActionType} [action] - The action the sort is applied on. Defaults to the query action.
|
|
57
|
+
* @return {Object[]} The `orderBy` entries, ending in a unique one.
|
|
58
|
+
* @throws {HttpError} 400 under the same conditions as {@link mapSortValues}.
|
|
59
|
+
*/
|
|
60
|
+
function mapStableSortValues(sort, resourceName, action = 'query') {
|
|
61
|
+
const orderBy = mapSortValues(sort, resourceName, action);
|
|
62
|
+
if (!orderBy.some((entry) => 'id' in entry)) {
|
|
63
|
+
orderBy.push({ id: 'asc' });
|
|
64
|
+
}
|
|
65
|
+
return orderBy;
|
|
66
|
+
}
|
|
49
67
|
/**
|
|
50
68
|
* Flattens a sort input into the list of its field paths and directions, keeping the order the fields were declared
|
|
51
69
|
* in. String inputs are split on commas, with a `-` or `+` prefix selecting the direction, and object inputs are
|
|
@@ -26,7 +26,9 @@ exports.apiKeyAuth = (0, fastify_plugin_1.default)(async (server) => {
|
|
|
26
26
|
// Use configured delimiter to split an API key and separate ID from the
|
|
27
27
|
// rest of the key value
|
|
28
28
|
const apiKeyParts = sanitizedApiKey.split(common_1.config.SECURITY_API_KEY_DELIMITER);
|
|
29
|
-
|
|
29
|
+
// The id prefix is read back in the primary key type of the ApiKey model,
|
|
30
|
+
// which can be configured as a string like on any other model
|
|
31
|
+
const apiKeyId = (0, common_1.toResourceId)(apiKeyParts.shift() ?? '', (0, context_1.injectModel)('ApiKey', false)?.config?.id);
|
|
30
32
|
const apiKeyValue = apiKeyParts.join(common_1.config.SECURITY_API_KEY_DELIMITER);
|
|
31
33
|
const cacheKey = cacheService.buildCacheKey({
|
|
32
34
|
baseKey: `apikey:${apiKeyId}`,
|
|
@@ -35,6 +37,7 @@ exports.apiKeyAuth = (0, fastify_plugin_1.default)(async (server) => {
|
|
|
35
37
|
let apiKey = await cacheService.getCachedValue(cacheKey);
|
|
36
38
|
if (!apiKey) {
|
|
37
39
|
try {
|
|
40
|
+
// The generated client types the id after the configured primary key
|
|
38
41
|
apiKey = await db
|
|
39
42
|
.client()
|
|
40
43
|
.apiKey.findFirst({ where: { id: apiKeyId } });
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { AuthScope, AuthSource, AuthUser, RouteConfig } from '@appweaver/common';
|
|
1
|
+
import { AuthScope, AuthSource, AuthUser, ResourceId, RouteConfig } from '@appweaver/common';
|
|
2
2
|
import { AuthTokens, JwtPayload, UserAdditionalData } from '../types';
|
|
3
3
|
export declare class AuthService {
|
|
4
4
|
/**
|
|
5
5
|
* Finds an authenticated user by their unique identifier.
|
|
6
6
|
*
|
|
7
|
-
* @param {
|
|
7
|
+
* @param {ResourceId} id - The unique identifier of the authenticated user to find.
|
|
8
8
|
* @return {Promise<AuthUser | null>} A promise that resolves to the authenticated user object if found, otherwise
|
|
9
9
|
* null.
|
|
10
10
|
*/
|
|
11
|
-
findById(id:
|
|
11
|
+
findById(id: ResourceId): Promise<AuthUser | null>;
|
|
12
12
|
/**
|
|
13
13
|
* Retrieves a user by their username.
|
|
14
14
|
*
|
|
@@ -19,12 +19,12 @@ export declare class AuthService {
|
|
|
19
19
|
/**
|
|
20
20
|
* Updates an authenticated user's information in the system.
|
|
21
21
|
*
|
|
22
|
-
* @param {
|
|
22
|
+
* @param {ResourceId} id - The unique identifier of the authenticated user to be updated.
|
|
23
23
|
* @param {Partial<AuthUser> & { password?: string }} data - The partial user data to update, optionally including a
|
|
24
24
|
* password.
|
|
25
25
|
* @return {Promise<AuthUser>} A promise that resolves to the updated authenticated user object.
|
|
26
26
|
*/
|
|
27
|
-
updateAuthUser(id:
|
|
27
|
+
updateAuthUser(id: ResourceId, data: Partial<AuthUser> & {
|
|
28
28
|
password?: string;
|
|
29
29
|
}): Promise<AuthUser>;
|
|
30
30
|
/**
|
|
@@ -112,9 +112,9 @@ export declare class AuthService {
|
|
|
112
112
|
/**
|
|
113
113
|
* Logs out a user by updating their authentication information with a logout timestamp.
|
|
114
114
|
*
|
|
115
|
-
* @param {
|
|
115
|
+
* @param {ResourceId} id - The unique identifier of the user to be logged out.
|
|
116
116
|
* @return {Promise<boolean>} A promise that resolves to a boolean indicating whether the logout operation was
|
|
117
117
|
* successful.
|
|
118
118
|
*/
|
|
119
|
-
logout(id:
|
|
119
|
+
logout(id: ResourceId): Promise<boolean>;
|
|
120
120
|
}
|