@appsemble/node-utils 0.20.44 → 0.21.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.
Files changed (103) hide show
  1. package/README.md +3 -3
  2. package/app.d.ts +17 -0
  3. package/app.js +49 -0
  4. package/csv.d.ts +2 -0
  5. package/csv.js +55 -0
  6. package/fs.d.ts +3 -3
  7. package/fs.test.js +5 -5
  8. package/getAppsembleMessages.d.ts +9 -0
  9. package/getAppsembleMessages.js +32 -0
  10. package/handleError.test.js +4 -4
  11. package/icon.d.ts +44 -0
  12. package/icon.js +69 -0
  13. package/index.d.ts +22 -1
  14. package/index.js +22 -1
  15. package/interceptors.test.js +5 -6
  16. package/jsonschema.d.ts +2 -0
  17. package/jsonschema.js +7 -0
  18. package/koa.d.ts +6 -0
  19. package/koa.js +27 -0
  20. package/logger.js +3 -0
  21. package/mergeMessages.d.ts +9 -0
  22. package/mergeMessages.js +17 -0
  23. package/middleware/boom.d.ts +7 -0
  24. package/middleware/boom.js +32 -0
  25. package/middleware/boom.test.js +58 -0
  26. package/middleware/conditional.d.ts +2 -0
  27. package/middleware/conditional.js +9 -0
  28. package/middleware/frontend.d.ts +3 -0
  29. package/middleware/frontend.js +40 -0
  30. package/{loggerMiddleware.js → middleware/loggerMiddleware.js} +1 -1
  31. package/{loggerMiddleware.test.js → middleware/loggerMiddleware.test.js} +14 -15
  32. package/odata.d.ts +18 -0
  33. package/odata.js +45 -0
  34. package/organizationBlocklist.d.ts +4 -0
  35. package/organizationBlocklist.js +5 -0
  36. package/package.json +41 -4
  37. package/parsers.d.ts +3 -0
  38. package/parsers.js +18 -0
  39. package/readAsset.d.ts +13 -0
  40. package/readAsset.js +6 -0
  41. package/readFileOrString.d.ts +1 -1
  42. package/render.d.ts +21 -0
  43. package/render.js +46 -0
  44. package/resource.d.ts +43 -0
  45. package/resource.js +172 -0
  46. package/scim.d.ts +26 -0
  47. package/scim.js +32 -0
  48. package/server/controllers/action.d.ts +7 -0
  49. package/server/controllers/action.js +129 -0
  50. package/server/controllers/appMembers.d.ts +3 -0
  51. package/server/controllers/appMembers.js +19 -0
  52. package/server/controllers/appMessages.d.ts +3 -0
  53. package/server/controllers/appMessages.js +75 -0
  54. package/server/controllers/assets.d.ts +5 -0
  55. package/server/controllers/assets.js +74 -0
  56. package/server/controllers/index.d.ts +7 -0
  57. package/server/controllers/index.js +8 -0
  58. package/server/controllers/oauth2Provider.d.ts +3 -0
  59. package/server/controllers/oauth2Provider.js +7 -0
  60. package/server/controllers/resources.d.ts +8 -0
  61. package/server/controllers/resources.js +273 -0
  62. package/server/controllers/resources.test.d.ts +1 -0
  63. package/server/controllers/resources.test.js +552 -0
  64. package/server/controllers/teams.d.ts +3 -0
  65. package/server/controllers/teams.js +12 -0
  66. package/server/routes/appRouter/blockAssetHandler.d.ts +3 -0
  67. package/server/routes/appRouter/blockAssetHandler.js +13 -0
  68. package/server/routes/appRouter/blockCssHandler.d.ts +3 -0
  69. package/server/routes/appRouter/blockCssHandler.js +15 -0
  70. package/server/routes/appRouter/bulmaHandler.d.ts +3 -0
  71. package/server/routes/appRouter/bulmaHandler.js +116 -0
  72. package/server/routes/appRouter/cssHandler.d.ts +3 -0
  73. package/server/routes/appRouter/cssHandler.js +12 -0
  74. package/server/routes/appRouter/iconHandler.d.ts +3 -0
  75. package/server/routes/appRouter/iconHandler.js +23 -0
  76. package/server/routes/appRouter/index.d.ts +3 -0
  77. package/server/routes/appRouter/index.js +85 -0
  78. package/server/routes/appRouter/indexHandler.d.ts +5 -0
  79. package/server/routes/appRouter/indexHandler.js +69 -0
  80. package/server/routes/appRouter/manifestHandler.d.ts +3 -0
  81. package/server/routes/appRouter/manifestHandler.js +47 -0
  82. package/server/routes/appRouter/robotsHandler.d.ts +2 -0
  83. package/server/routes/appRouter/robotsHandler.js +6 -0
  84. package/server/routes/appRouter/screenshotHandler.d.ts +3 -0
  85. package/server/routes/appRouter/screenshotHandler.js +19 -0
  86. package/server/routes/appRouter/serviceWorkerHandler.d.ts +3 -0
  87. package/server/routes/appRouter/serviceWorkerHandler.js +18 -0
  88. package/server/routes/appRouter/staticHandler.d.ts +2 -0
  89. package/server/routes/appRouter/staticHandler.js +9 -0
  90. package/server/types.d.ts +356 -0
  91. package/server/types.js +2 -0
  92. package/testFixtures.d.ts +3 -3
  93. package/testFixtures.js +1 -1
  94. package/tinyRouter.d.ts +16 -0
  95. package/tinyRouter.js +34 -0
  96. package/tinyRouter.test.d.ts +1 -0
  97. package/tinyRouter.test.js +85 -0
  98. package/vitest.config.d.ts +2 -0
  99. package/vitest.setup.d.ts +1 -0
  100. package/{jest.setup.js → vitest.setup.js} +1 -1
  101. /package/{jest.setup.d.ts → middleware/boom.test.d.ts} +0 -0
  102. /package/{loggerMiddleware.d.ts → middleware/loggerMiddleware.d.ts} +0 -0
  103. /package/{loggerMiddleware.test.d.ts → middleware/loggerMiddleware.test.d.ts} +0 -0
package/resource.js ADDED
@@ -0,0 +1,172 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { handleValidatorResult } from '@appsemble/node-utils';
3
+ import { notFound } from '@hapi/boom';
4
+ import { addMilliseconds, isPast, parseISO } from 'date-fns';
5
+ import { ValidationError, Validator } from 'jsonschema';
6
+ import parseDuration from 'parse-duration';
7
+ import { preProcessCSV } from './csv.js';
8
+ function stripResource({ $author, $created, $editor, $updated, ...data }) {
9
+ return data;
10
+ }
11
+ /**
12
+ * Get the resource definition of an app by name.
13
+ *
14
+ * If there is no match, a 404 HTTP error is thrown.
15
+ *
16
+ * @param app The app to get the resource definition of
17
+ * @param resourceType The name of the resource definition to get.
18
+ * @param view The view that’s being used.
19
+ * @returns The matching resource definition.
20
+ */
21
+ export function getResourceDefinition(app, resourceType, view) {
22
+ var _a;
23
+ if (!app) {
24
+ throw notFound('App not found');
25
+ }
26
+ const definition = (_a = app.definition.resources) === null || _a === void 0 ? void 0 : _a[resourceType];
27
+ if (!definition) {
28
+ throw notFound(`App does not have resources called ${resourceType}`);
29
+ }
30
+ if (view && !definition.views[view]) {
31
+ throw notFound(`View ${view} does not exist for resource type ${resourceType}`);
32
+ }
33
+ return definition;
34
+ }
35
+ /**
36
+ * Extracts the IDs of resource request body.
37
+ *
38
+ * @param ctx The Koa context to extract the body from.
39
+ * @returns A tuple which consists of:
40
+ *
41
+ * 1. One or more resources processed from the request body.
42
+ * 2. A list of newly uploaded assets which should be linked to the resources.
43
+ * 3. preValidateProperty function used for reconstructing resources from a CSV file.
44
+ */
45
+ export function extractResourceBody(ctx) {
46
+ let body;
47
+ let assets;
48
+ let preValidateProperty;
49
+ if (ctx.is('multipart/form-data')) {
50
+ ({ assets = [], resource: body } = ctx.request.body);
51
+ if (Array.isArray(body) && body.length === 1) {
52
+ [body] = body;
53
+ }
54
+ }
55
+ else {
56
+ if (ctx.is('text/csv')) {
57
+ preValidateProperty = preProcessCSV;
58
+ }
59
+ ({ body } = ctx.request);
60
+ assets = [];
61
+ }
62
+ return [
63
+ Array.isArray(body) ? body.map(stripResource) : stripResource(body),
64
+ assets,
65
+ preValidateProperty,
66
+ ];
67
+ }
68
+ /**
69
+ * Process an incoming resource request body.
70
+ *
71
+ * This handles JSON schema validation, resource expiration, and asset linking and validation.
72
+ *
73
+ * @param ctx The Koa context to process.
74
+ * @param definition The resource definition to use for processing the request body.
75
+ * @param knownAssetIds A list of asset IDs that are already known to be linked to the resource.
76
+ * @param knownExpires A previously known expires value.
77
+ * @returns A tuple which consists of:
78
+ *
79
+ * 1. One or more resources processed from the request body.
80
+ * 2. A list of newly uploaded assets which should be linked to the resources.
81
+ * 3. Asset IDs from the `knownAssetIds` array which are no longer used.
82
+ */
83
+ export function processResourceBody(ctx, definition, knownAssetIds = [], knownExpires) {
84
+ const [resource, assets, preValidateProperty] = extractResourceBody(ctx);
85
+ const validator = new Validator();
86
+ const assetIdMap = new Map();
87
+ const assetUsedMap = new Map();
88
+ const reusedAssets = new Set();
89
+ const preparedAssets = assets.map(({ contents, filename, mime }, index) => {
90
+ const id = randomUUID();
91
+ assetIdMap.set(index, id);
92
+ assetUsedMap.set(index, false);
93
+ return { data: contents, filename, id, mime };
94
+ });
95
+ validator.customFormats.binary = (input) => {
96
+ if (knownAssetIds.includes(input)) {
97
+ reusedAssets.add(input);
98
+ return true;
99
+ }
100
+ if (!/^\d+$/.test(input)) {
101
+ return false;
102
+ }
103
+ const num = Number(input);
104
+ if (assetUsedMap.get(num)) {
105
+ return false;
106
+ }
107
+ return num >= 0 && num < assets.length;
108
+ };
109
+ const patchedSchema = {
110
+ ...definition.schema,
111
+ required: ctx.request.method === 'PATCH' ? [] : definition.schema.required,
112
+ properties: {
113
+ ...definition.schema.properties,
114
+ id: { type: 'integer' },
115
+ $expires: { type: 'string', format: 'date-time' },
116
+ $clonable: { type: 'boolean' },
117
+ },
118
+ };
119
+ const customErrors = [];
120
+ const expiresDuration = definition.expires ? parseDuration(definition.expires) : undefined;
121
+ const result = validator.validate(resource, Array.isArray(resource) ? { type: 'array', items: patchedSchema } : patchedSchema, {
122
+ base: '#',
123
+ preValidateProperty,
124
+ nestedErrors: true,
125
+ rewrite(value, { format }, options, { path }) {
126
+ if (Array.isArray(resource)
127
+ ? path.length === 2 && typeof path[0] === 'number' && path[1] === '$expires'
128
+ : path.length === 1 && path[0] === '$expires') {
129
+ if (value !== undefined) {
130
+ const date = parseISO(value);
131
+ if (isPast(date)) {
132
+ customErrors.push(new ValidationError('has already passed', value, null, path));
133
+ }
134
+ return date;
135
+ }
136
+ if (knownExpires) {
137
+ return knownExpires;
138
+ }
139
+ if (expiresDuration) {
140
+ return addMilliseconds(new Date(), expiresDuration);
141
+ }
142
+ }
143
+ if (value === undefined) {
144
+ return;
145
+ }
146
+ if (format !== 'binary') {
147
+ return value;
148
+ }
149
+ if (knownAssetIds.includes(value)) {
150
+ return value;
151
+ }
152
+ const num = Number(value);
153
+ if (!assetIdMap.has(num)) {
154
+ return value;
155
+ }
156
+ const uuid = assetIdMap.get(num);
157
+ const currentResource = Array.isArray(resource) ? resource[path[0]] : resource;
158
+ preparedAssets.find((asset) => asset.id === uuid).resource = currentResource;
159
+ assetUsedMap.set(num, true);
160
+ return uuid;
161
+ },
162
+ });
163
+ result.errors.push(...customErrors);
164
+ for (const [assetId, used] of assetUsedMap.entries()) {
165
+ if (!used) {
166
+ result.errors.push(new ValidationError('is not referenced from the resource', assetId, null, ['assets', assetId], 'binary', 'format'));
167
+ }
168
+ }
169
+ handleValidatorResult(result, 'Resource validation failed');
170
+ return [resource, preparedAssets, knownAssetIds.filter((id) => !reusedAssets.has(id))];
171
+ }
172
+ //# sourceMappingURL=resource.js.map
package/scim.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The SCIM protocol uses the HTTP response status codes defined in
3
+ * [Section 6 of [RFC7231]](https://datatracker.ietf.org/doc/html/rfc7231#section-6) to indicate
4
+ * operation success or failure. In addition to returning an HTTP response code, implementers
5
+ * **MUST** return the errors in the body of the response in a JSON format, using the attributes
6
+ * described below. Error responses are identified using the following "schema" URI:
7
+ * `"urn:ietf:params:scim:api:messages:2.0:Error"`.
8
+ *
9
+ * @see https://datatracker.ietf.org/doc/html/rfc7644#section-3.12
10
+ */
11
+ export declare class SCIMError extends Error {
12
+ schemas: string[];
13
+ /**
14
+ * The HTTP status code (see
15
+ * [Section 6 of [RFC7231]](https://datatracker.ietf.org/doc/html/rfc7231#section-6)) expressed as
16
+ * a JSON string.
17
+ */
18
+ status: string;
19
+ /**
20
+ * A detailed human-readable message.
21
+ */
22
+ detail: string;
23
+ constructor(status: number, detail: string);
24
+ toJSON(): Pick<SCIMError, 'detail' | 'schemas' | 'status'>;
25
+ }
26
+ export declare function scimAssert(condition: unknown, status: number, detail: string): asserts condition;
package/scim.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The SCIM protocol uses the HTTP response status codes defined in
3
+ * [Section 6 of [RFC7231]](https://datatracker.ietf.org/doc/html/rfc7231#section-6) to indicate
4
+ * operation success or failure. In addition to returning an HTTP response code, implementers
5
+ * **MUST** return the errors in the body of the response in a JSON format, using the attributes
6
+ * described below. Error responses are identified using the following "schema" URI:
7
+ * `"urn:ietf:params:scim:api:messages:2.0:Error"`.
8
+ *
9
+ * @see https://datatracker.ietf.org/doc/html/rfc7644#section-3.12
10
+ */
11
+ export class SCIMError extends Error {
12
+ constructor(status, detail) {
13
+ super(detail);
14
+ this.schemas = ['urn:ietf:params:scim:api:messages:2.0:Error'];
15
+ this.detail = detail;
16
+ this.name = 'SCIMError';
17
+ this.status = String(status);
18
+ }
19
+ toJSON() {
20
+ return {
21
+ detail: this.detail,
22
+ schemas: this.schemas,
23
+ status: this.status,
24
+ };
25
+ }
26
+ }
27
+ export function scimAssert(condition, status, detail) {
28
+ if (!condition) {
29
+ throw new SCIMError(status, detail);
30
+ }
31
+ }
32
+ //# sourceMappingURL=scim.js.map
@@ -0,0 +1,7 @@
1
+ import { type Options } from '@appsemble/node-utils';
2
+ import { type Middleware } from 'koa';
3
+ export declare function createProxyDelete(options: Options): Middleware;
4
+ export declare function createProxyGet(options: Options): Middleware;
5
+ export declare function createProxyPatch(options: Options): Middleware;
6
+ export declare function createProxyPost(options: Options): Middleware;
7
+ export declare function createProxyPut(options: Options): Middleware;
@@ -0,0 +1,129 @@
1
+ import { getRemapperContext, logger } from '@appsemble/node-utils';
2
+ import { defaultLocale, formatRequestAction, remap } from '@appsemble/utils';
3
+ import { badGateway, badRequest, methodNotAllowed, notFound } from '@hapi/boom';
4
+ import axios from 'axios';
5
+ import { get, pick } from 'lodash-es';
6
+ import pkg from '../../package.json' assert { type: 'json' };
7
+ /**
8
+ * These response headers are forwarded when proxying requests.
9
+ */
10
+ const allowResponseHeaders = [
11
+ 'content-encoding',
12
+ 'content-length',
13
+ 'content-type',
14
+ 'transfer-encoding',
15
+ ];
16
+ const supportedActions = ['email', 'notify', 'request'];
17
+ async function handleEmail(ctx, app, action, options) {
18
+ const { mailer, method, request: { body: data }, user, } = ctx;
19
+ if (method !== 'POST') {
20
+ throw methodNotAllowed('Method must be POST for email actions');
21
+ }
22
+ const { email, reloadUser } = options;
23
+ await reloadUser({ context: ctx });
24
+ await email({ action, data, mailer, user, options, context: ctx });
25
+ ctx.status = 204;
26
+ }
27
+ async function handleNotify(ctx, app, action, options) {
28
+ var _a, _b;
29
+ const { request: { body: data }, user, } = ctx;
30
+ const { reloadUser, sendNotifications } = options;
31
+ await reloadUser({ context: ctx });
32
+ const remapperContext = await getRemapperContext(app, app.definition.defaultLanguage || defaultLocale, user && {
33
+ sub: user.id,
34
+ name: user.name,
35
+ email: user.primaryEmail,
36
+ email_verified: Boolean((_b = (_a = user.EmailAuthorizations) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.verified),
37
+ zoneinfo: user.timezone,
38
+ }, options, ctx);
39
+ const to = remap(action.to, data, remapperContext);
40
+ const title = remap(action.title, data, remapperContext);
41
+ const body = remap(action.body, data, remapperContext);
42
+ await sendNotifications({ app, to, title, body });
43
+ ctx.status = 204;
44
+ }
45
+ async function handleRequestProxy(ctx, app, action, useBody, options) {
46
+ var _a, _b;
47
+ const { method, query, request: { body }, user, } = ctx;
48
+ let data;
49
+ if (useBody) {
50
+ data = body;
51
+ }
52
+ else {
53
+ try {
54
+ data = JSON.parse(query.data);
55
+ }
56
+ catch {
57
+ throw badRequest('data should be a JSON object');
58
+ }
59
+ }
60
+ const { applyAppServiceSecrets, reloadUser } = options;
61
+ await reloadUser({ context: ctx });
62
+ const remapperContext = await getRemapperContext(app, app.definition.defaultLanguage || defaultLocale, user && {
63
+ sub: user.id,
64
+ name: user.name,
65
+ email: user.primaryEmail,
66
+ email_verified: Boolean((_b = (_a = user.EmailAuthorizations) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.verified),
67
+ zoneinfo: user.timezone,
68
+ }, options, ctx);
69
+ let axiosConfig = formatRequestAction(action, data, (remapper, d) => remap(remapper, d, remapperContext), remapperContext.context);
70
+ axiosConfig = await applyAppServiceSecrets({ axiosConfig, context: ctx, app });
71
+ if (axiosConfig.method.toUpperCase() !== method) {
72
+ throw badRequest('Method does match the request action method');
73
+ }
74
+ if (useBody) {
75
+ axiosConfig.data = data;
76
+ }
77
+ axiosConfig.headers['user-agent'] = `AppsembleServer/${pkg.version}`;
78
+ axiosConfig.responseType = 'stream';
79
+ axiosConfig.validateStatus = () => true;
80
+ let response;
81
+ logger.verbose(`Forwarding request to ${axios.getUri(axiosConfig)}`);
82
+ try {
83
+ response = await axios(axiosConfig);
84
+ }
85
+ catch (err) {
86
+ logger.error(err);
87
+ throw badGateway();
88
+ }
89
+ ctx.status = response.status;
90
+ ctx.set(pick(response.headers, allowResponseHeaders));
91
+ ctx.body = response.data;
92
+ }
93
+ function createProxyHandler(useBody, options) {
94
+ return async (ctx) => {
95
+ const { pathParams: { appId, path }, } = ctx;
96
+ const app = await options.getApp({ context: ctx, query: { where: { id: appId } } });
97
+ if (!app) {
98
+ throw notFound('App not found');
99
+ }
100
+ const appAction = get(app.definition, path);
101
+ const action = supportedActions.find((act) => act === (appAction === null || appAction === void 0 ? void 0 : appAction.type));
102
+ switch (action) {
103
+ case 'email':
104
+ return handleEmail(ctx, app, appAction, options);
105
+ case 'notify':
106
+ return handleNotify(ctx, app, appAction, options);
107
+ case 'request':
108
+ return handleRequestProxy(ctx, app, appAction, useBody, options);
109
+ default:
110
+ throw badRequest('path does not point to a proxyable action');
111
+ }
112
+ };
113
+ }
114
+ export function createProxyDelete(options) {
115
+ return createProxyHandler(false, options);
116
+ }
117
+ export function createProxyGet(options) {
118
+ return createProxyHandler(false, options);
119
+ }
120
+ export function createProxyPatch(options) {
121
+ return createProxyHandler(true, options);
122
+ }
123
+ export function createProxyPost(options) {
124
+ return createProxyHandler(true, options);
125
+ }
126
+ export function createProxyPut(options) {
127
+ return createProxyHandler(true, options);
128
+ }
129
+ //# sourceMappingURL=action.js.map
@@ -0,0 +1,3 @@
1
+ import { type Middleware } from 'koa';
2
+ import { type Options } from '../types.js';
3
+ export declare function createGetAppMember({ getApp, getAppMembers }: Options): Middleware;
@@ -0,0 +1,19 @@
1
+ import { notFound } from '@hapi/boom';
2
+ export function createGetAppMember({ getApp, getAppMembers }) {
3
+ return async (ctx) => {
4
+ const { pathParams: { appId, memberId }, } = ctx;
5
+ const app = await getApp({ context: ctx, query: { where: { id: appId } } });
6
+ if (!app) {
7
+ throw notFound('App not found');
8
+ }
9
+ if (app.definition.security === undefined) {
10
+ throw notFound('App does not have a security definition');
11
+ }
12
+ const appMembers = await getAppMembers({ context: ctx, app, memberId });
13
+ if (appMembers.length !== 1) {
14
+ throw notFound('App member not found');
15
+ }
16
+ ctx.body = appMembers[0];
17
+ };
18
+ }
19
+ //# sourceMappingURL=appMembers.js.map
@@ -0,0 +1,3 @@
1
+ import { type Options } from '@appsemble/node-utils';
2
+ import { type Middleware } from 'koa';
3
+ export declare function createGetMessages({ getApp, getAppMessages, getBlockMessages, }: Options): Middleware;
@@ -0,0 +1,75 @@
1
+ import { getAppsembleMessages, mergeMessages, } from '@appsemble/node-utils';
2
+ import { defaultLocale, extractAppMessages, normalizeBlockName, } from '@appsemble/utils';
3
+ import { badRequest, notFound } from '@hapi/boom';
4
+ import tags from 'language-tags';
5
+ export function createGetMessages({ getApp, getAppMessages, getBlockMessages, }) {
6
+ return async (ctx) => {
7
+ var _a, _b, _c, _d, _e;
8
+ const { pathParams: { appId, language }, query: { merge, override = 'true' }, } = ctx;
9
+ if (!tags.check(language)) {
10
+ throw badRequest(`Language “${language}” is invalid`);
11
+ }
12
+ const lang = language.toLowerCase();
13
+ const baseLanguage = tags(language)
14
+ .subtags()
15
+ .find((sub) => sub.type() === 'language');
16
+ const baseLang = baseLanguage && String(baseLanguage).toLowerCase();
17
+ const app = await getApp({ context: ctx, query: { where: { id: appId } } });
18
+ if (!app) {
19
+ throw notFound('App not found');
20
+ }
21
+ const appMessages = await getAppMessages({ context: ctx, app, language });
22
+ const blockPrefixes = [];
23
+ const blockQuery = [];
24
+ const coreMessages = await getAppsembleMessages(lang, baseLang);
25
+ const messages = {
26
+ core: Object.fromEntries(Object.entries(coreMessages).filter(([key]) => key.startsWith('app') || key.startsWith('react-components') || key.startsWith('server'))),
27
+ blocks: {},
28
+ ...extractAppMessages(app.definition, (block, prefix) => {
29
+ const blockName = normalizeBlockName(block.type);
30
+ const [org, name] = blockName.split('/');
31
+ blockQuery.push({ version: block.version, OrganizationId: org.slice(1), name });
32
+ blockPrefixes.push([blockName, prefix]);
33
+ }),
34
+ };
35
+ const blockMessages = await getBlockMessages({ context: ctx, blockQuery, baseLang, lang });
36
+ if ((!appMessages.length || (merge && !appMessages.some((m) => m.language === lang))) &&
37
+ lang !== (app.definition.defaultLanguage || defaultLocale)) {
38
+ throw notFound(`Language “${language}” could not be found`);
39
+ }
40
+ const baseLanguageMessages = override === 'true' && appMessages.find((m) => m.language === baseLang);
41
+ const languageMessages = override === 'true' && appMessages.find((m) => m.language === lang);
42
+ for (const block of blockMessages) {
43
+ const { name } = block;
44
+ const defaultMessages = (_a = block.messages) === null || _a === void 0 ? void 0 : _a[defaultLocale];
45
+ const blockBaseLanguageMessages = baseLang && ((_b = block.messages) === null || _b === void 0 ? void 0 : _b[baseLang]);
46
+ const blockLanguageMessages = (_c = block.messages) === null || _c === void 0 ? void 0 : _c[language];
47
+ const blockVersionMessages = {
48
+ ...defaultMessages,
49
+ ...Object.fromEntries(Object.entries(blockBaseLanguageMessages !== null && blockBaseLanguageMessages !== void 0 ? blockBaseLanguageMessages : {}).filter(([, value]) => value)),
50
+ ...Object.fromEntries(Object.entries(blockLanguageMessages !== null && blockLanguageMessages !== void 0 ? blockLanguageMessages : {}).filter(([, value]) => value)),
51
+ };
52
+ if (messages.blocks[name]) {
53
+ messages.blocks[name][block.version] = blockVersionMessages;
54
+ }
55
+ else {
56
+ messages.blocks[name] = {
57
+ [block.version]: blockVersionMessages,
58
+ };
59
+ }
60
+ if (override !== 'true') {
61
+ const prefixed = blockPrefixes.filter(([b]) => b === name);
62
+ for (const [messageId, value] of Object.entries(blockVersionMessages)) {
63
+ for (const [, prefix] of prefixed) {
64
+ messages.app[`${prefix.join('.')}.${messageId}`] = value;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ctx.body = {
70
+ language: lang,
71
+ messages: mergeMessages(messages, (_d = baseLanguageMessages === null || baseLanguageMessages === void 0 ? void 0 : baseLanguageMessages.messages) !== null && _d !== void 0 ? _d : {}, (_e = languageMessages === null || languageMessages === void 0 ? void 0 : languageMessages.messages) !== null && _e !== void 0 ? _e : {}),
72
+ };
73
+ };
74
+ }
75
+ //# sourceMappingURL=appMessages.js.map
@@ -0,0 +1,5 @@
1
+ import { type Options } from '@appsemble/node-utils';
2
+ import { type Middleware } from 'koa';
3
+ export declare function createGetAssets({ checkRole, getApp, getAppAssets }: Options): Middleware;
4
+ export declare function createGetAssetById({ getApp, getAppAssets }: Options): Middleware;
5
+ export declare function createCreateAsset({ createAppAsset, getApp }: Options): Middleware;
@@ -0,0 +1,74 @@
1
+ import { Permission } from '@appsemble/utils';
2
+ import { notFound } from '@hapi/boom';
3
+ import { extension } from 'mime-types';
4
+ export function createGetAssets({ checkRole, getApp, getAppAssets }) {
5
+ return async (ctx) => {
6
+ const { pathParams: { appId }, queryParams: { $skip, $top }, } = ctx;
7
+ const app = await getApp({ context: ctx, query: { where: { id: appId } } });
8
+ if (!app) {
9
+ throw notFound('App not found');
10
+ }
11
+ const findOptions = {
12
+ limit: $top,
13
+ offset: $skip,
14
+ where: { AppId: app.id },
15
+ };
16
+ await checkRole({ context: ctx, app, permissions: Permission.ReadAssets, findOptions });
17
+ ctx.body = await getAppAssets({ app, context: ctx });
18
+ };
19
+ }
20
+ export function createGetAssetById({ getApp, getAppAssets }) {
21
+ return async (ctx) => {
22
+ const { pathParams: { appId, assetId }, } = ctx;
23
+ const app = await getApp({ context: ctx, query: { where: { id: appId } } });
24
+ if (!app) {
25
+ throw notFound('App not found');
26
+ }
27
+ const assets = await getAppAssets({ app, context: ctx });
28
+ // Pick asset id over asset name
29
+ const asset = assets.find((a) => a.id === assetId) || assets.find((a) => a.name === assetId);
30
+ if (!asset) {
31
+ throw notFound('Asset not found');
32
+ }
33
+ if (assetId !== asset.id) {
34
+ // Redirect to asset using current asset ID
35
+ ctx.status = 302;
36
+ ctx.set('location', `/api/apps/${app.id}/assets/${asset.id}`);
37
+ ctx.type = null;
38
+ return;
39
+ }
40
+ let { filename, mime } = asset;
41
+ if (!filename) {
42
+ filename = asset.id;
43
+ if (mime) {
44
+ const ext = extension(mime);
45
+ if (ext) {
46
+ filename += `.${ext}`;
47
+ }
48
+ }
49
+ }
50
+ ctx.set('content-type', mime || 'application/octet-stream');
51
+ if (filename) {
52
+ ctx.set('content-disposition', `attachment; filename=${JSON.stringify(filename)}`);
53
+ }
54
+ ctx.set('Cache-Control', 'max-age=31536000,immutable');
55
+ ctx.body = asset.data;
56
+ };
57
+ }
58
+ export function createCreateAsset({ createAppAsset, getApp }) {
59
+ return async (ctx) => {
60
+ const { pathParams: { appId }, request: { body: { file: { contents, filename, mime }, name, }, }, } = ctx;
61
+ const app = await getApp({ context: ctx, query: { where: { id: appId } } });
62
+ if (!app) {
63
+ throw notFound('App not found');
64
+ }
65
+ const asset = await createAppAsset({
66
+ app,
67
+ context: ctx,
68
+ payload: { filename, mime, name, data: contents },
69
+ });
70
+ ctx.status = 201;
71
+ ctx.body = { id: asset.id, mime, filename, name };
72
+ };
73
+ }
74
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1,7 @@
1
+ export * from './action.js';
2
+ export * from './appMessages.js';
3
+ export * from './assets.js';
4
+ export * from './resources.js';
5
+ export * from './appMembers.js';
6
+ export * from './teams.js';
7
+ export * from './oauth2Provider.js';
@@ -0,0 +1,8 @@
1
+ export * from './action.js';
2
+ export * from './appMessages.js';
3
+ export * from './assets.js';
4
+ export * from './resources.js';
5
+ export * from './appMembers.js';
6
+ export * from './teams.js';
7
+ export * from './oauth2Provider.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { type Middleware } from 'koa';
2
+ import { type Options } from '../types.js';
3
+ export declare function createGetUserInfo({ getAppUserInfo }: Options): Middleware;
@@ -0,0 +1,7 @@
1
+ export function createGetUserInfo({ getAppUserInfo }) {
2
+ return async (ctx) => {
3
+ const { client, user } = ctx;
4
+ ctx.body = await getAppUserInfo({ context: ctx, client, user });
5
+ };
6
+ }
7
+ //# sourceMappingURL=oauth2Provider.js.map
@@ -0,0 +1,8 @@
1
+ import { type Options } from '@appsemble/node-utils';
2
+ import { type Middleware } from 'koa';
3
+ export declare function createQueryResources(options: Options): Middleware;
4
+ export declare function createCountResources(options: Options): Middleware;
5
+ export declare function createGetResourceById(options: Options): Middleware;
6
+ export declare function createCreateResource(options: Options): Middleware;
7
+ export declare function createUpdateResource(options: Options): Middleware;
8
+ export declare function createDeleteResource(options: Options): Middleware;