@appsemble/node-utils 0.24.0 → 0.24.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/README.md +3 -3
- package/UserPropertiesError.d.ts +3 -0
- package/UserPropertiesError.js +8 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +3 -3
- package/resource.js +30 -5
- package/server/controllers/resources.js +63 -12
- package/server/controllers/resources.test.js +6 -0
- package/server/types.d.ts +4 -0
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Node Utilities
|
|
2
2
|
|
|
3
3
|
> NodeJS utilities used by Appsemble internally.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/node-utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.24.1)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
|
|
|
26
26
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.24.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.24.1/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -33,5 +33,6 @@ export * from './koa.js';
|
|
|
33
33
|
export { bulmaVersion, faVersion, version } from './versions.cjs';
|
|
34
34
|
export * from './EmailQuotaExceededError.js';
|
|
35
35
|
export * from './EmailError.js';
|
|
36
|
+
export * from './UserPropertiesError.js';
|
|
36
37
|
export * from './authentication.js';
|
|
37
38
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/node-utils",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "NodeJS utilities used by Appsemble internally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"test": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@appsemble/types": "0.24.
|
|
42
|
-
"@appsemble/utils": "0.24.
|
|
41
|
+
"@appsemble/types": "0.24.1",
|
|
42
|
+
"@appsemble/utils": "0.24.1",
|
|
43
43
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
44
44
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
45
45
|
"@koa/cors": "^4.0.0",
|
package/resource.js
CHANGED
|
@@ -128,8 +128,18 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
|
|
|
128
128
|
properties: {
|
|
129
129
|
...definition.schema.properties,
|
|
130
130
|
id: { type: 'integer' },
|
|
131
|
-
$expires: {
|
|
131
|
+
$expires: {
|
|
132
|
+
anyOf: [
|
|
133
|
+
{ type: 'string', format: 'date-time' },
|
|
134
|
+
{
|
|
135
|
+
type: 'string',
|
|
136
|
+
pattern: /^(\d+(y|yr|years))?\s*(\d+months)?\s*(\d+(w|wk|weeks))?\s*(\d+(d|days))?\s*(\d+(h|hr|hours))?\s*(\d+(m|min|minutes))?\s*(\d+(s|sec|seconds))?$/
|
|
137
|
+
.source,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
132
141
|
$clonable: { type: 'boolean' },
|
|
142
|
+
$ephemeral: { type: 'boolean' },
|
|
133
143
|
},
|
|
134
144
|
};
|
|
135
145
|
const customErrors = [];
|
|
@@ -139,12 +149,21 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
|
|
|
139
149
|
preValidateProperty,
|
|
140
150
|
nestedErrors: true,
|
|
141
151
|
rewrite(value, { format }, options, { path }) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
let propertyName;
|
|
153
|
+
if (Array.isArray(resource) && path.length === 2 && typeof path[0] === 'number') {
|
|
154
|
+
propertyName = path[1];
|
|
155
|
+
}
|
|
156
|
+
else if (path.length === 1) {
|
|
157
|
+
propertyName = path[0];
|
|
158
|
+
}
|
|
159
|
+
if (propertyName === '$expires') {
|
|
145
160
|
if (value !== undefined) {
|
|
146
161
|
const date = parseISO(value);
|
|
147
|
-
if (
|
|
162
|
+
if (Number.isNaN(date.getTime())) {
|
|
163
|
+
return addMilliseconds(new Date(), parseDuration(value));
|
|
164
|
+
}
|
|
165
|
+
if (isPast(date) &&
|
|
166
|
+
!customErrors.some((error) => error.message === 'has already passed' && error.path === path)) {
|
|
148
167
|
customErrors.push(new ValidationError('has already passed', value, null, path));
|
|
149
168
|
}
|
|
150
169
|
return date;
|
|
@@ -157,6 +176,12 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
|
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
if (value === undefined) {
|
|
179
|
+
if (propertyName === '$clonable') {
|
|
180
|
+
return definition.clonable;
|
|
181
|
+
}
|
|
182
|
+
if (propertyName === '$ephemeral') {
|
|
183
|
+
return definition.ephemeral;
|
|
184
|
+
}
|
|
160
185
|
return;
|
|
161
186
|
}
|
|
162
187
|
if (format !== 'binary') {
|
|
@@ -39,6 +39,7 @@ export function createQueryResources(options) {
|
|
|
39
39
|
type: resourceType,
|
|
40
40
|
AppId: appId,
|
|
41
41
|
expires: { or: [{ gt: new Date() }, null] },
|
|
42
|
+
...(app.demoMode ? { seed: false, ephemeral: true } : {}),
|
|
42
43
|
},
|
|
43
44
|
],
|
|
44
45
|
},
|
|
@@ -90,6 +91,7 @@ export function createCountResources(options) {
|
|
|
90
91
|
type: resourceType,
|
|
91
92
|
AppId: appId,
|
|
92
93
|
expires: { or: [{ gt: new Date() }, null] },
|
|
94
|
+
...(app.demoMode ? { seed: false, ephemeral: true } : {}),
|
|
93
95
|
},
|
|
94
96
|
],
|
|
95
97
|
},
|
|
@@ -127,6 +129,7 @@ export function createGetResourceById(options) {
|
|
|
127
129
|
type: resourceType,
|
|
128
130
|
AppId: appId,
|
|
129
131
|
expires: { or: [{ gt: new Date() }, null] },
|
|
132
|
+
...(app.demoMode ? { seed: false, ephemeral: true } : {}),
|
|
130
133
|
},
|
|
131
134
|
};
|
|
132
135
|
const resource = await getAppResource({
|
|
@@ -154,9 +157,10 @@ export function createGetResourceById(options) {
|
|
|
154
157
|
export function createCreateResource(options) {
|
|
155
158
|
return async (ctx) => {
|
|
156
159
|
const { pathParams: { appId, resourceType }, } = ctx;
|
|
157
|
-
const { createAppResourcesWithAssets, getApp, verifyResourceActionPermission } = options;
|
|
160
|
+
const { checkSeededResources, createAppResourcesWithAssets, getApp, verifyResourceActionPermission, } = options;
|
|
158
161
|
const action = 'create';
|
|
159
162
|
const app = await getApp({ context: ctx, query: { where: { id: appId } } });
|
|
163
|
+
const resourceSeeded = await checkSeededResources({ context: ctx, app, resourceType });
|
|
160
164
|
const resourceDefinition = getResourceDefinition(app, resourceType, ctx);
|
|
161
165
|
await verifyResourceActionPermission({ app, context: ctx, action, resourceType, options, ctx });
|
|
162
166
|
const [processedBody, preparedAssets] = processResourceBody(ctx, resourceDefinition);
|
|
@@ -165,15 +169,57 @@ export function createCreateResource(options) {
|
|
|
165
169
|
return;
|
|
166
170
|
}
|
|
167
171
|
const resources = Array.isArray(processedBody) ? processedBody : [processedBody];
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
let createdResources;
|
|
173
|
+
// In demo apps, there are two types of resources.
|
|
174
|
+
// If the app has not been seeded, resources passed from the request body
|
|
175
|
+
// are interpreted as seed resources and created with "seed: true". In addition,
|
|
176
|
+
// copies of these resources are created with "ephemeral: true".
|
|
177
|
+
// If the app has already been seeded, only new ephemeral resources are created,
|
|
178
|
+
// processed from the request body.
|
|
179
|
+
if (app.demoMode) {
|
|
180
|
+
if (!resourceSeeded) {
|
|
181
|
+
await createAppResourcesWithAssets({
|
|
182
|
+
app,
|
|
183
|
+
context: ctx,
|
|
184
|
+
resources: resources.map((resource) => ({
|
|
185
|
+
...resource,
|
|
186
|
+
$seed: true,
|
|
187
|
+
$ephemeral: false,
|
|
188
|
+
})),
|
|
189
|
+
preparedAssets,
|
|
190
|
+
resourceType,
|
|
191
|
+
action,
|
|
192
|
+
options,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
createdResources = await createAppResourcesWithAssets({
|
|
196
|
+
app,
|
|
197
|
+
context: ctx,
|
|
198
|
+
resources: resources.map((resource) => ({
|
|
199
|
+
...resource,
|
|
200
|
+
$seed: false,
|
|
201
|
+
$ephemeral: true,
|
|
202
|
+
$clonable: false,
|
|
203
|
+
})),
|
|
204
|
+
preparedAssets,
|
|
205
|
+
resourceType,
|
|
206
|
+
action,
|
|
207
|
+
options,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// In regular apps, resources are created as usual
|
|
212
|
+
// with "clonable", "ephemeral" and "expires" passed from the request body
|
|
213
|
+
createdResources = await createAppResourcesWithAssets({
|
|
214
|
+
app,
|
|
215
|
+
context: ctx,
|
|
216
|
+
resources,
|
|
217
|
+
preparedAssets,
|
|
218
|
+
resourceType,
|
|
219
|
+
action,
|
|
220
|
+
options,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
177
223
|
ctx.body = Array.isArray(processedBody) ? createdResources : createdResources[0];
|
|
178
224
|
};
|
|
179
225
|
}
|
|
@@ -199,6 +245,7 @@ export function createUpdateResource(options) {
|
|
|
199
245
|
type: resourceType,
|
|
200
246
|
AppId: appId,
|
|
201
247
|
expires: { or: [{ gt: new Date() }, null] },
|
|
248
|
+
...(app.demoMode ? { seed: false, ephemeral: true } : {}),
|
|
202
249
|
},
|
|
203
250
|
};
|
|
204
251
|
const oldResource = await getAppResource({
|
|
@@ -217,7 +264,10 @@ export function createUpdateResource(options) {
|
|
|
217
264
|
context: ctx,
|
|
218
265
|
id: resourceId,
|
|
219
266
|
type: resourceType,
|
|
220
|
-
resource:
|
|
267
|
+
resource: {
|
|
268
|
+
...resources[0],
|
|
269
|
+
...(app.demoMode ? { $ephemeral: true, $clonable: false } : {}),
|
|
270
|
+
},
|
|
221
271
|
preparedAssets,
|
|
222
272
|
deletedAssetIds,
|
|
223
273
|
resourceDefinition,
|
|
@@ -246,7 +296,8 @@ export function createDeleteResource(options) {
|
|
|
246
296
|
id: resourceId,
|
|
247
297
|
type: resourceType,
|
|
248
298
|
AppId: appId,
|
|
249
|
-
expires: { or: [{ gt: new Date() }, null] },
|
|
299
|
+
expires: { or: [{ gt: new Date() }, { eq: null }] },
|
|
300
|
+
...(app.demoMode ? { seed: false, ephemeral: true } : {}),
|
|
250
301
|
},
|
|
251
302
|
};
|
|
252
303
|
const resource = await getAppResource({
|
|
@@ -11,6 +11,7 @@ let mockParseQuery;
|
|
|
11
11
|
let mockGetAppUrl;
|
|
12
12
|
let mockGetAppMessages;
|
|
13
13
|
let mockCreateAppResourcesWithAssets;
|
|
14
|
+
let mockCheckSeededResources;
|
|
14
15
|
let mockCtx;
|
|
15
16
|
let mockCtxIs;
|
|
16
17
|
describe('createQueryResources', () => {
|
|
@@ -427,6 +428,7 @@ describe('createCreateResource', () => {
|
|
|
427
428
|
mockGetApp = vi.fn();
|
|
428
429
|
mockVerifyResourceActionPermission = vi.fn();
|
|
429
430
|
mockCreateAppResourcesWithAssets = vi.fn();
|
|
431
|
+
mockCheckSeededResources = vi.fn();
|
|
430
432
|
mockCtxIs = vi.fn();
|
|
431
433
|
mockCtx = {
|
|
432
434
|
pathParams: { appId: 1, resourceType: 'mockResourceType' },
|
|
@@ -449,10 +451,12 @@ describe('createCreateResource', () => {
|
|
|
449
451
|
mockGetApp.mockResolvedValue(mockApp);
|
|
450
452
|
mockCtxIs.mockReturnValue('multipart/form-data');
|
|
451
453
|
mockCreateAppResourcesWithAssets.mockResolvedValue(mockResources);
|
|
454
|
+
mockCheckSeededResources.mockResolvedValue(false);
|
|
452
455
|
const middleware = createCreateResource({
|
|
453
456
|
getApp: mockGetApp,
|
|
454
457
|
createAppResourcesWithAssets: mockCreateAppResourcesWithAssets,
|
|
455
458
|
verifyResourceActionPermission: mockVerifyResourceActionPermission,
|
|
459
|
+
checkSeededResources: mockCheckSeededResources,
|
|
456
460
|
});
|
|
457
461
|
await middleware(mockCtx, vi.fn());
|
|
458
462
|
expect(mockGetApp).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -506,10 +510,12 @@ describe('createCreateResource', () => {
|
|
|
506
510
|
mockGetApp.mockResolvedValue(mockApp);
|
|
507
511
|
mockCtxIs.mockReturnValue('multipart/form-data');
|
|
508
512
|
mockCreateAppResourcesWithAssets.mockResolvedValue(mockResources);
|
|
513
|
+
mockCheckSeededResources.mockResolvedValue(false);
|
|
509
514
|
const middleware = createCreateResource({
|
|
510
515
|
getApp: mockGetApp,
|
|
511
516
|
createAppResourcesWithAssets: mockCreateAppResourcesWithAssets,
|
|
512
517
|
verifyResourceActionPermission: mockVerifyResourceActionPermission,
|
|
518
|
+
checkSeededResources: mockCheckSeededResources,
|
|
513
519
|
});
|
|
514
520
|
await middleware(mockCtx, vi.fn());
|
|
515
521
|
expect(mockGetApp).toHaveBeenCalledWith(expect.objectContaining({
|
package/server/types.d.ts
CHANGED
|
@@ -124,6 +124,9 @@ export interface GetAppSubEntityParams {
|
|
|
124
124
|
context: ParameterizedContext<DefaultState, DefaultContextInterface, any>;
|
|
125
125
|
app: App;
|
|
126
126
|
}
|
|
127
|
+
export interface CheckSeededResourcesParams extends GetAppSubEntityParams {
|
|
128
|
+
resourceType: string;
|
|
129
|
+
}
|
|
127
130
|
export interface GetAppMessagesParams extends GetAppSubEntityParams {
|
|
128
131
|
language?: string;
|
|
129
132
|
merge?: string[] | string;
|
|
@@ -372,5 +375,6 @@ export interface Options {
|
|
|
372
375
|
deleteAppAsset: (params: DeleteAppAssetParams) => Promise<number>;
|
|
373
376
|
email: (params: EmailParams) => Promise<void>;
|
|
374
377
|
sendNotifications: (params: SendNotificationsParams) => Promise<void>;
|
|
378
|
+
checkSeededResources: (params: CheckSeededResourcesParams) => Promise<boolean>;
|
|
375
379
|
}
|
|
376
380
|
export {};
|