@appsemble/node-utils 0.23.9 → 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.
@@ -0,0 +1,3 @@
1
+ export declare class EmailError extends Error {
2
+ constructor(message: string);
3
+ }
package/EmailError.js ADDED
@@ -0,0 +1,8 @@
1
+ export class EmailError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'EmailError';
5
+ Error.captureStackTrace(this, EmailError);
6
+ }
7
+ }
8
+ //# sourceMappingURL=EmailError.js.map
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.23.9/config/assets/logo.svg) Appsemble Node Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.24.1/config/assets/logo.svg) Appsemble Node Utilities
2
2
 
3
3
  > NodeJS utilities used by Appsemble internally.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/node-utils)](https://www.npmjs.com/package/@appsemble/node-utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.23.9/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.23.9)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.24.1/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.24.1)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](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.23.9/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.24.1/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
@@ -0,0 +1,3 @@
1
+ export declare class UserPropertiesError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,8 @@
1
+ export class UserPropertiesError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UserPropertiesError';
5
+ Error.captureStackTrace(this, UserPropertiesError);
6
+ }
7
+ }
8
+ //# sourceMappingURL=UserPropertiesError.js.map
package/app.d.ts CHANGED
@@ -10,7 +10,7 @@ import { type Options } from './server/types.js';
10
10
  * @param app The app for which to get the remapper context.
11
11
  * @param language The preferred language for the context.
12
12
  * @param userInfo The OAuth2 compatible user information.
13
- * @param options The utility options.
13
+ * @param options The API utility options.
14
14
  * @param context The koa context.
15
15
  * @returns A localized remapper context for the app.
16
16
  */
package/app.js CHANGED
@@ -11,7 +11,7 @@ const getPluralRules = memoize((locale, opts) => new Intl.PluralRules(locale, op
11
11
  * @param app The app for which to get the remapper context.
12
12
  * @param language The preferred language for the context.
13
13
  * @param userInfo The OAuth2 compatible user information.
14
- * @param options The utility options.
14
+ * @param options The API utility options.
15
15
  * @param context The koa context.
16
16
  * @returns A localized remapper context for the app.
17
17
  */
package/index.d.ts CHANGED
@@ -32,4 +32,6 @@ export * from './server/types.js';
32
32
  export * from './koa.js';
33
33
  export { bulmaVersion, faVersion, version } from './versions.cjs';
34
34
  export * from './EmailQuotaExceededError.js';
35
+ export * from './EmailError.js';
36
+ export * from './UserPropertiesError.js';
35
37
  export * from './authentication.js';
package/index.js CHANGED
@@ -32,5 +32,7 @@ export * from './server/types.js';
32
32
  export * from './koa.js';
33
33
  export { bulmaVersion, faVersion, version } from './versions.cjs';
34
34
  export * from './EmailQuotaExceededError.js';
35
+ export * from './EmailError.js';
36
+ export * from './UserPropertiesError.js';
35
37
  export * from './authentication.js';
36
38
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/node-utils",
3
- "version": "0.23.9",
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.23.9",
42
- "@appsemble/utils": "0.23.9",
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: { type: 'string', format: 'date-time' },
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
- if (Array.isArray(resource)
143
- ? path.length === 2 && typeof path[0] === 'number' && path[1] === '$expires'
144
- : path.length === 1 && path[0] === '$expires') {
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 (isPast(date)) {
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') {
@@ -1,5 +1,5 @@
1
1
  import { assertKoaError, getRemapperContext, logger, throwKoaError, version, } from '@appsemble/node-utils';
2
- import { defaultLocale, formatRequestAction, remap } from '@appsemble/utils';
2
+ import { defaultLocale, remap } from '@appsemble/utils';
3
3
  import axios from 'axios';
4
4
  import { get, pick } from 'lodash-es';
5
5
  import { EmailQuotaExceededError } from '../../EmailQuotaExceededError.js';
@@ -50,7 +50,7 @@ async function handleNotify(ctx, app, action, options) {
50
50
  ctx.status = 204;
51
51
  }
52
52
  async function handleRequestProxy(ctx, app, action, useBody, options) {
53
- var _a, _b;
53
+ var _a, _b, _c;
54
54
  const { method, query, request: { body, headers }, user, } = ctx;
55
55
  let data;
56
56
  if (useBody) {
@@ -73,7 +73,22 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
73
73
  email_verified: Boolean((_b = (_a = user.EmailAuthorizations) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.verified),
74
74
  zoneinfo: user.timezone,
75
75
  }, options, ctx);
76
- let axiosConfig = formatRequestAction(action, data, (remapper, d) => remap(remapper, d, remapperContext), remapperContext.context);
76
+ let params;
77
+ try {
78
+ if (query.params) {
79
+ params = JSON.parse(query.params);
80
+ }
81
+ }
82
+ catch {
83
+ throwKoaError(ctx, 400, 'params should be a JSON object.');
84
+ }
85
+ let axiosConfig = {
86
+ method: (_c = action.method) !== null && _c !== void 0 ? _c : 'GET',
87
+ url: String(remap(action.url, data, remapperContext)),
88
+ params,
89
+ responseType: 'arraybuffer',
90
+ headers: {},
91
+ };
77
92
  axiosConfig = await applyAppServiceSecrets({ axiosConfig, context: ctx, app });
78
93
  if (axiosConfig.method.toUpperCase() !== method) {
79
94
  throwKoaError(ctx, 400, 'Method does not match the request action method');
@@ -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
- const createdResources = await createAppResourcesWithAssets({
169
- app,
170
- context: ctx,
171
- resources,
172
- preparedAssets,
173
- resourceType,
174
- action,
175
- options,
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: resources[0],
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
@@ -79,6 +79,7 @@ declare module 'koas-parameters' {
79
79
  token: string;
80
80
  appServiceId: number;
81
81
  userId: string;
82
+ memberEmail: string;
82
83
  trainingBlockId: number;
83
84
  trainingId: number;
84
85
  }
@@ -95,6 +96,7 @@ declare module 'koas-parameters' {
95
96
  startIndex: number;
96
97
  view: string;
97
98
  resources: boolean;
99
+ roles: string[];
98
100
  }
99
101
  }
100
102
  export type Operator = 'and' | 'not' | 'or';
@@ -122,6 +124,9 @@ export interface GetAppSubEntityParams {
122
124
  context: ParameterizedContext<DefaultState, DefaultContextInterface, any>;
123
125
  app: App;
124
126
  }
127
+ export interface CheckSeededResourcesParams extends GetAppSubEntityParams {
128
+ resourceType: string;
129
+ }
125
130
  export interface GetAppMessagesParams extends GetAppSubEntityParams {
126
131
  language?: string;
127
132
  merge?: string[] | string;
@@ -370,5 +375,6 @@ export interface Options {
370
375
  deleteAppAsset: (params: DeleteAppAssetParams) => Promise<number>;
371
376
  email: (params: EmailParams) => Promise<void>;
372
377
  sendNotifications: (params: SendNotificationsParams) => Promise<void>;
378
+ checkSeededResources: (params: CheckSeededResourcesParams) => Promise<boolean>;
373
379
  }
374
380
  export {};