@appsemble/node-utils 0.20.45 → 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.
- package/README.md +3 -3
- package/app.d.ts +17 -0
- package/app.js +49 -0
- package/csv.d.ts +2 -0
- package/csv.js +55 -0
- package/fs.d.ts +3 -3
- package/fs.test.js +5 -5
- package/getAppsembleMessages.d.ts +9 -0
- package/getAppsembleMessages.js +32 -0
- package/handleError.test.js +4 -4
- package/icon.d.ts +44 -0
- package/icon.js +69 -0
- package/index.d.ts +22 -1
- package/index.js +22 -1
- package/interceptors.test.js +5 -6
- package/jsonschema.d.ts +2 -0
- package/jsonschema.js +7 -0
- package/koa.d.ts +6 -0
- package/koa.js +27 -0
- package/mergeMessages.d.ts +9 -0
- package/mergeMessages.js +17 -0
- package/middleware/boom.d.ts +7 -0
- package/middleware/boom.js +32 -0
- package/middleware/boom.test.js +58 -0
- package/middleware/conditional.d.ts +2 -0
- package/middleware/conditional.js +9 -0
- package/middleware/frontend.d.ts +3 -0
- package/middleware/frontend.js +40 -0
- package/{loggerMiddleware.js → middleware/loggerMiddleware.js} +1 -1
- package/{loggerMiddleware.test.js → middleware/loggerMiddleware.test.js} +14 -15
- package/odata.d.ts +18 -0
- package/odata.js +45 -0
- package/organizationBlocklist.d.ts +4 -0
- package/organizationBlocklist.js +5 -0
- package/package.json +41 -4
- package/parsers.d.ts +3 -0
- package/parsers.js +18 -0
- package/readAsset.d.ts +13 -0
- package/readAsset.js +6 -0
- package/readFileOrString.d.ts +1 -1
- package/render.d.ts +21 -0
- package/render.js +46 -0
- package/resource.d.ts +43 -0
- package/resource.js +172 -0
- package/scim.d.ts +26 -0
- package/scim.js +32 -0
- package/server/controllers/action.d.ts +7 -0
- package/server/controllers/action.js +129 -0
- package/server/controllers/appMembers.d.ts +3 -0
- package/server/controllers/appMembers.js +19 -0
- package/server/controllers/appMessages.d.ts +3 -0
- package/server/controllers/appMessages.js +75 -0
- package/server/controllers/assets.d.ts +5 -0
- package/server/controllers/assets.js +74 -0
- package/server/controllers/index.d.ts +7 -0
- package/server/controllers/index.js +8 -0
- package/server/controllers/oauth2Provider.d.ts +3 -0
- package/server/controllers/oauth2Provider.js +7 -0
- package/server/controllers/resources.d.ts +8 -0
- package/server/controllers/resources.js +273 -0
- package/server/controllers/resources.test.d.ts +1 -0
- package/server/controllers/resources.test.js +552 -0
- package/server/controllers/teams.d.ts +3 -0
- package/server/controllers/teams.js +12 -0
- package/server/routes/appRouter/blockAssetHandler.d.ts +3 -0
- package/server/routes/appRouter/blockAssetHandler.js +13 -0
- package/server/routes/appRouter/blockCssHandler.d.ts +3 -0
- package/server/routes/appRouter/blockCssHandler.js +15 -0
- package/server/routes/appRouter/bulmaHandler.d.ts +3 -0
- package/server/routes/appRouter/bulmaHandler.js +116 -0
- package/server/routes/appRouter/cssHandler.d.ts +3 -0
- package/server/routes/appRouter/cssHandler.js +12 -0
- package/server/routes/appRouter/iconHandler.d.ts +3 -0
- package/server/routes/appRouter/iconHandler.js +23 -0
- package/server/routes/appRouter/index.d.ts +3 -0
- package/server/routes/appRouter/index.js +85 -0
- package/server/routes/appRouter/indexHandler.d.ts +5 -0
- package/server/routes/appRouter/indexHandler.js +69 -0
- package/server/routes/appRouter/manifestHandler.d.ts +3 -0
- package/server/routes/appRouter/manifestHandler.js +47 -0
- package/server/routes/appRouter/robotsHandler.d.ts +2 -0
- package/server/routes/appRouter/robotsHandler.js +6 -0
- package/server/routes/appRouter/screenshotHandler.d.ts +3 -0
- package/server/routes/appRouter/screenshotHandler.js +19 -0
- package/server/routes/appRouter/serviceWorkerHandler.d.ts +3 -0
- package/server/routes/appRouter/serviceWorkerHandler.js +18 -0
- package/server/routes/appRouter/staticHandler.d.ts +2 -0
- package/server/routes/appRouter/staticHandler.js +9 -0
- package/server/types.d.ts +356 -0
- package/server/types.js +2 -0
- package/testFixtures.d.ts +3 -3
- package/testFixtures.js +1 -1
- package/tinyRouter.d.ts +16 -0
- package/tinyRouter.js +34 -0
- package/tinyRouter.test.d.ts +1 -0
- package/tinyRouter.test.js +85 -0
- package/vitest.config.d.ts +2 -0
- package/vitest.setup.d.ts +1 -0
- package/{jest.setup.js → vitest.setup.js} +1 -1
- /package/{jest.setup.d.ts → middleware/boom.test.d.ts} +0 -0
- /package/{loggerMiddleware.d.ts → middleware/loggerMiddleware.d.ts} +0 -0
- /package/{loggerMiddleware.test.d.ts → middleware/loggerMiddleware.test.d.ts} +0 -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.21.0)
|
|
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.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.21.0/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/app.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type App, type UserInfo } from '@appsemble/types';
|
|
2
|
+
import { type RemapperContext } from '@appsemble/utils';
|
|
3
|
+
import { type DefaultContext, type DefaultState, type ParameterizedContext } from 'koa';
|
|
4
|
+
import { type Options } from './server/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Get a context for remappers based on an app definition.
|
|
7
|
+
*
|
|
8
|
+
* This allows to use remappers with the context of an app on the server.
|
|
9
|
+
*
|
|
10
|
+
* @param app The app for which to get the remapper context.
|
|
11
|
+
* @param language The preferred language for the context.
|
|
12
|
+
* @param userInfo The OAuth2 compatible user information.
|
|
13
|
+
* @param options The utility options.
|
|
14
|
+
* @param context The koa context.
|
|
15
|
+
* @returns A localized remapper context for the app.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getRemapperContext(app: App, language: string, userInfo: UserInfo, options: Options, context: ParameterizedContext<DefaultState, DefaultContext, any>): Promise<RemapperContext>;
|
package/app.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defaultLocale, has, objectCache } from '@appsemble/utils';
|
|
2
|
+
import { memoize } from '@formatjs/fast-memoize';
|
|
3
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
4
|
+
const getNumberFormat = memoize((locale, opts) => new Intl.NumberFormat(locale, opts));
|
|
5
|
+
const getPluralRules = memoize((locale, opts) => new Intl.PluralRules(locale, opts));
|
|
6
|
+
/**
|
|
7
|
+
* Get a context for remappers based on an app definition.
|
|
8
|
+
*
|
|
9
|
+
* This allows to use remappers with the context of an app on the server.
|
|
10
|
+
*
|
|
11
|
+
* @param app The app for which to get the remapper context.
|
|
12
|
+
* @param language The preferred language for the context.
|
|
13
|
+
* @param userInfo The OAuth2 compatible user information.
|
|
14
|
+
* @param options The utility options.
|
|
15
|
+
* @param context The koa context.
|
|
16
|
+
* @returns A localized remapper context for the app.
|
|
17
|
+
*/
|
|
18
|
+
export async function getRemapperContext(app, language, userInfo, options, context) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
const { getAppMessages, getAppUrl } = options;
|
|
21
|
+
const appUrl = String(await getAppUrl({ context, app }));
|
|
22
|
+
const appMessages = await getAppMessages({
|
|
23
|
+
app,
|
|
24
|
+
context,
|
|
25
|
+
language,
|
|
26
|
+
});
|
|
27
|
+
const cache = objectCache((message) => new IntlMessageFormat(message, language, undefined, {
|
|
28
|
+
formatters: {
|
|
29
|
+
// @ts-expect-error intl-messageformat types are wrong
|
|
30
|
+
getNumberFormat,
|
|
31
|
+
getPluralRules,
|
|
32
|
+
getDateTimeFormat: memoize((locale, opts) => new Intl.DateTimeFormat(locale, { ...opts, timeZone: userInfo === null || userInfo === void 0 ? void 0 : userInfo.zoneinfo })),
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
appId: app.id,
|
|
37
|
+
appUrl,
|
|
38
|
+
url: appUrl,
|
|
39
|
+
getMessage({ defaultMessage, id }) {
|
|
40
|
+
const msg = appMessages.find(({ messages }) => has(messages.messageIds, id));
|
|
41
|
+
const message = msg ? msg.messages.messageIds[id] : defaultMessage;
|
|
42
|
+
return cache(message || `'{${id}}'`);
|
|
43
|
+
},
|
|
44
|
+
userInfo,
|
|
45
|
+
context: {},
|
|
46
|
+
locale: (_b = (_a = userInfo === null || userInfo === void 0 ? void 0 : userInfo.locale) !== null && _a !== void 0 ? _a : app.definition.defaultLanguage) !== null && _b !== void 0 ? _b : defaultLocale,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=app.js.map
|
package/csv.d.ts
ADDED
package/csv.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const preProcessCSV = (instance, key, schema, options, ctx) => {
|
|
2
|
+
// CSV is parsed into an array of flat objects. This means all instances are a direct descendant
|
|
3
|
+
// of the root array and the path length is 1.
|
|
4
|
+
if (ctx.path.length !== 1) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const parent = instance;
|
|
8
|
+
const value = instance[key];
|
|
9
|
+
switch (schema.type) {
|
|
10
|
+
case 'null':
|
|
11
|
+
if (value === 'null') {
|
|
12
|
+
parent[key] = null;
|
|
13
|
+
}
|
|
14
|
+
break;
|
|
15
|
+
case 'boolean':
|
|
16
|
+
if (value === 'true') {
|
|
17
|
+
parent[key] = true;
|
|
18
|
+
}
|
|
19
|
+
else if (value === 'false') {
|
|
20
|
+
parent[key] = false;
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case 'integer': {
|
|
24
|
+
const result = Number(value);
|
|
25
|
+
if (Number.isInteger(result)) {
|
|
26
|
+
parent[key] = result;
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'number': {
|
|
31
|
+
const result = Number(value);
|
|
32
|
+
if (Number.isFinite(result)) {
|
|
33
|
+
parent[key] = result;
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'array': {
|
|
38
|
+
const result = JSON.parse(value);
|
|
39
|
+
if (Array.isArray(result)) {
|
|
40
|
+
parent[key] = result;
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'object': {
|
|
45
|
+
const result = JSON.parse(value);
|
|
46
|
+
if (result && typeof result === 'object') {
|
|
47
|
+
parent[key] = result;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
default:
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=csv.js.map
|
package/fs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
/// <reference types="node" />
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
4
4
|
import { type Dirent } from 'node:fs';
|
|
5
5
|
import { type Promisable } from 'type-fest';
|
|
6
6
|
/**
|
package/fs.test.js
CHANGED
|
@@ -40,7 +40,7 @@ describe('readData', () => {
|
|
|
40
40
|
});
|
|
41
41
|
describe('opendirSafe', () => {
|
|
42
42
|
it('should read a directory', async () => {
|
|
43
|
-
const onFile =
|
|
43
|
+
const onFile = vi.fn();
|
|
44
44
|
await opendirSafe(resolveFixture('test'), onFile);
|
|
45
45
|
expect(onFile).toHaveBeenCalledTimes(3);
|
|
46
46
|
expect(onFile).toHaveBeenCalledWith(resolveFixture('test/bar.txt'), expect.objectContaining({ name: 'bar.txt' }));
|
|
@@ -48,7 +48,7 @@ describe('opendirSafe', () => {
|
|
|
48
48
|
expect(onFile).toHaveBeenCalledWith(resolveFixture('test/z'), expect.objectContaining({ name: 'z' }));
|
|
49
49
|
});
|
|
50
50
|
it('should read a directory recursively if specified', async () => {
|
|
51
|
-
const onFile =
|
|
51
|
+
const onFile = vi.fn();
|
|
52
52
|
await opendirSafe(resolveFixture('test'), onFile, { recursive: true });
|
|
53
53
|
expect(onFile).toHaveBeenCalledTimes(5);
|
|
54
54
|
expect(onFile).toHaveBeenCalledWith(resolveFixture('test/bar.txt'), expect.objectContaining({ name: 'bar.txt' }));
|
|
@@ -59,15 +59,15 @@ describe('opendirSafe', () => {
|
|
|
59
59
|
});
|
|
60
60
|
it('should throw if the path is not a directory', async () => {
|
|
61
61
|
const path = resolveFixture('hello.txt');
|
|
62
|
-
await expect(opendirSafe(path,
|
|
62
|
+
await expect(opendirSafe(path, vi.fn())).rejects.toThrow(new AppsembleError(`Expected ${path} to be a directory`));
|
|
63
63
|
});
|
|
64
64
|
it('should throw if the path doesn’t exist', async () => {
|
|
65
65
|
const path = resolveFixture('non-existent');
|
|
66
|
-
await expect(opendirSafe(path,
|
|
66
|
+
await expect(opendirSafe(path, vi.fn())).rejects.toThrow(new AppsembleError(`Expected ${path} to be a directory`));
|
|
67
67
|
});
|
|
68
68
|
it('should not throw if the path doesn’t exist and allowMissing is true', async () => {
|
|
69
69
|
const path = resolveFixture('non-existent');
|
|
70
|
-
const result = await opendirSafe(path,
|
|
70
|
+
const result = await opendirSafe(path, vi.fn(), { allowMissing: true });
|
|
71
71
|
expect(result).toBeUndefined();
|
|
72
72
|
});
|
|
73
73
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function getSupportedLanguages(): Promise<Set<string>>;
|
|
2
|
+
/**
|
|
3
|
+
* Fetch and merge the Appsemble core messages based on a language and a base language.
|
|
4
|
+
*
|
|
5
|
+
* @param language The language to get the messages of.
|
|
6
|
+
* @param baseLanguage The base language of the language to get the messages of.
|
|
7
|
+
* @returns The Appsemble core messages.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getAppsembleMessages(language: string, baseLanguage?: string): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { defaultLocale } from '@appsemble/utils';
|
|
3
|
+
const translationsDir = new URL('../../i18n/', import.meta.url);
|
|
4
|
+
export async function getSupportedLanguages() {
|
|
5
|
+
const files = await readdir(translationsDir);
|
|
6
|
+
return new Set(files.map((lang) => lang.split('.json')[0].toLowerCase()));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fetch and merge the Appsemble core messages based on a language and a base language.
|
|
10
|
+
*
|
|
11
|
+
* @param language The language to get the messages of.
|
|
12
|
+
* @param baseLanguage The base language of the language to get the messages of.
|
|
13
|
+
* @returns The Appsemble core messages.
|
|
14
|
+
*/
|
|
15
|
+
export async function getAppsembleMessages(language, baseLanguage) {
|
|
16
|
+
const lang = language.toLowerCase();
|
|
17
|
+
const baseLang = baseLanguage === null || baseLanguage === void 0 ? void 0 : baseLanguage.toLowerCase();
|
|
18
|
+
const languages = await getSupportedLanguages();
|
|
19
|
+
const messages = {};
|
|
20
|
+
if (baseLang && languages.has(baseLanguage)) {
|
|
21
|
+
Object.assign(messages, JSON.parse(await readFile(new URL(`${baseLang}.json`, translationsDir), 'utf8')));
|
|
22
|
+
}
|
|
23
|
+
if (languages.has(lang)) {
|
|
24
|
+
Object.assign(messages, Object.fromEntries(Object.entries(JSON.parse(await readFile(new URL(`${lang}.json`, translationsDir), 'utf8'))).filter(([, value]) => Boolean(value))));
|
|
25
|
+
}
|
|
26
|
+
// Fall back to reading the default locale’s messages if no core messages were found.
|
|
27
|
+
if (!languages.has(lang) && (!baseLang || !languages.has(baseLang))) {
|
|
28
|
+
Object.assign(messages, JSON.parse(await readFile(new URL(`${defaultLocale}.json`, translationsDir), 'utf8')));
|
|
29
|
+
}
|
|
30
|
+
return messages;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=getAppsembleMessages.js.map
|
package/handleError.test.js
CHANGED
|
@@ -3,10 +3,10 @@ import { handleError } from './handleError.js';
|
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
let yargs;
|
|
5
5
|
beforeEach(() => {
|
|
6
|
-
yargs = { showHelp:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
yargs = { showHelp: vi.fn() };
|
|
7
|
+
vi.spyOn(console, 'error').mockImplementation(null);
|
|
8
|
+
vi.spyOn(process, 'exit').mockImplementation(() => null);
|
|
9
|
+
vi.spyOn(logger, 'error').mockImplementation(null);
|
|
10
10
|
});
|
|
11
11
|
it('should show the help message if it is passed a yargs message', () => {
|
|
12
12
|
handleError('Command foo not found', undefined, yargs);
|
package/icon.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { type Context } from 'koa';
|
|
3
|
+
import { type RGBA } from 'sharp';
|
|
4
|
+
interface ServeIconOptions {
|
|
5
|
+
/**
|
|
6
|
+
* If specified, fill the background with this color.
|
|
7
|
+
*/
|
|
8
|
+
background?: RGBA | string;
|
|
9
|
+
/**
|
|
10
|
+
* If true, cache the icon for a week / immutable.
|
|
11
|
+
*/
|
|
12
|
+
cache?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The name of a fallback icon to use.
|
|
15
|
+
*/
|
|
16
|
+
fallback: string;
|
|
17
|
+
/**
|
|
18
|
+
* The height to scale the image to.
|
|
19
|
+
*/
|
|
20
|
+
height?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The icon to render.
|
|
23
|
+
*/
|
|
24
|
+
icon?: Buffer;
|
|
25
|
+
/**
|
|
26
|
+
* If true, the maskable icon is preferred over the regular icon. If no maskable icon is defined,
|
|
27
|
+
* the icon will be scaled to fit within a maskable icon diameter (80% diameter).
|
|
28
|
+
*/
|
|
29
|
+
maskable?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The maskable icon to render if this is preferred.
|
|
32
|
+
*/
|
|
33
|
+
maskableIcon?: Buffer;
|
|
34
|
+
/**
|
|
35
|
+
* If true, render the raw icon. In this case all other options are ignored.
|
|
36
|
+
*/
|
|
37
|
+
raw?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The width to scale the image to.
|
|
40
|
+
*/
|
|
41
|
+
width?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function serveIcon(ctx: Context, { background, cache, fallback, height, icon, maskable, maskableIcon, raw, width, }: ServeIconOptions): Promise<void>;
|
|
44
|
+
export {};
|
package/icon.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { readAsset } from './readAsset.js';
|
|
3
|
+
/**
|
|
4
|
+
* A transparent background color used by sharp.
|
|
5
|
+
*/
|
|
6
|
+
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
7
|
+
/**
|
|
8
|
+
* The diameter of the safe area for maskable icons.
|
|
9
|
+
*
|
|
10
|
+
* https://css-tricks.com/maskable-icons-android-maskable-icons-for-your-pwa/
|
|
11
|
+
*/
|
|
12
|
+
const safeAreaDiameter = 0.8;
|
|
13
|
+
export async function serveIcon(ctx, { background, cache, fallback, height, icon, maskable, maskableIcon, raw, width, }) {
|
|
14
|
+
const buffer = (maskable && maskableIcon) || icon || (await readAsset(fallback));
|
|
15
|
+
let img = sharp(buffer);
|
|
16
|
+
if (raw) {
|
|
17
|
+
const { format } = await img.metadata();
|
|
18
|
+
ctx.body = buffer;
|
|
19
|
+
ctx.type = format;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!maskable) {
|
|
23
|
+
// Serve the regular app icon, but scaled.
|
|
24
|
+
img.resize({
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
fit: 'contain',
|
|
28
|
+
background: background || transparent,
|
|
29
|
+
});
|
|
30
|
+
if (background) {
|
|
31
|
+
img.flatten({ background });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (maskableIcon) {
|
|
35
|
+
img.resize({ width, height, fit: 'cover' });
|
|
36
|
+
if (background) {
|
|
37
|
+
img.flatten({ background });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Make the regular icon maskable
|
|
42
|
+
const actual = img;
|
|
43
|
+
const metadata = await actual.metadata();
|
|
44
|
+
const angle = Math.atan(metadata.height / metadata.width);
|
|
45
|
+
actual.resize({
|
|
46
|
+
width: Math.ceil(Math.cos(angle) * safeAreaDiameter * width),
|
|
47
|
+
// By leaving out height, libvips will determine this for us. This has better precision than
|
|
48
|
+
// calculating this using JavasScript and passing it manually.
|
|
49
|
+
// height: Math.ceil(Math.sin(angle) * safeAreaDiameter * size),
|
|
50
|
+
fit: 'contain',
|
|
51
|
+
background,
|
|
52
|
+
});
|
|
53
|
+
img = sharp(Buffer.alloc(width * height * 4, 0), {
|
|
54
|
+
raw: { width, height, channels: 4 },
|
|
55
|
+
});
|
|
56
|
+
img.resize({ width, height });
|
|
57
|
+
img.composite([{ input: await actual.toFormat('png').toBuffer() }]);
|
|
58
|
+
if (background) {
|
|
59
|
+
img.flatten({ background });
|
|
60
|
+
}
|
|
61
|
+
// Cache app icons for 1 week.
|
|
62
|
+
if (cache) {
|
|
63
|
+
ctx.set('cache-control', `public, max-age=${60 * 60 * 24 * 7},immutable`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ctx.body = await img.toFormat('png').toBuffer();
|
|
67
|
+
ctx.type = 'image/png';
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=icon.js.map
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,27 @@ export * from './getWorkspaces.js';
|
|
|
6
6
|
export * from './handleError.js';
|
|
7
7
|
export * from './interceptors.js';
|
|
8
8
|
export * from './logger.js';
|
|
9
|
-
export * from './loggerMiddleware.js';
|
|
9
|
+
export * from './middleware/loggerMiddleware.js';
|
|
10
|
+
export * from './middleware/boom.js';
|
|
11
|
+
export * from './middleware/conditional.js';
|
|
12
|
+
export * from './middleware/frontend.js';
|
|
13
|
+
export * from './parsers.js';
|
|
10
14
|
export * from './readFileOrString.js';
|
|
11
15
|
export * from './testFixtures.js';
|
|
16
|
+
export * from './tinyRouter.js';
|
|
17
|
+
export * from './icon.js';
|
|
18
|
+
export * from './readAsset.js';
|
|
19
|
+
export * from './render.js';
|
|
20
|
+
export * from './getAppsembleMessages.js';
|
|
21
|
+
export * from './odata.js';
|
|
22
|
+
export * from './resource.js';
|
|
23
|
+
export * from './app.js';
|
|
24
|
+
export * from './mergeMessages.js';
|
|
25
|
+
export * from './jsonschema.js';
|
|
26
|
+
export * from './organizationBlocklist.js';
|
|
27
|
+
export * from './scim.js';
|
|
28
|
+
export * from './server/routes/appRouter/index.js';
|
|
29
|
+
export * from './server/routes/appRouter/iconHandler.js';
|
|
30
|
+
export * from './server/controllers/index.js';
|
|
31
|
+
export * from './server/types.js';
|
|
32
|
+
export * from './koa.js';
|
package/index.js
CHANGED
|
@@ -6,7 +6,28 @@ export * from './getWorkspaces.js';
|
|
|
6
6
|
export * from './handleError.js';
|
|
7
7
|
export * from './interceptors.js';
|
|
8
8
|
export * from './logger.js';
|
|
9
|
-
export * from './loggerMiddleware.js';
|
|
9
|
+
export * from './middleware/loggerMiddleware.js';
|
|
10
|
+
export * from './middleware/boom.js';
|
|
11
|
+
export * from './middleware/conditional.js';
|
|
12
|
+
export * from './middleware/frontend.js';
|
|
13
|
+
export * from './parsers.js';
|
|
10
14
|
export * from './readFileOrString.js';
|
|
11
15
|
export * from './testFixtures.js';
|
|
16
|
+
export * from './tinyRouter.js';
|
|
17
|
+
export * from './icon.js';
|
|
18
|
+
export * from './readAsset.js';
|
|
19
|
+
export * from './render.js';
|
|
20
|
+
export * from './getAppsembleMessages.js';
|
|
21
|
+
export * from './odata.js';
|
|
22
|
+
export * from './resource.js';
|
|
23
|
+
export * from './app.js';
|
|
24
|
+
export * from './mergeMessages.js';
|
|
25
|
+
export * from './jsonschema.js';
|
|
26
|
+
export * from './organizationBlocklist.js';
|
|
27
|
+
export * from './scim.js';
|
|
28
|
+
export * from './server/routes/appRouter/index.js';
|
|
29
|
+
export * from './server/routes/appRouter/iconHandler.js';
|
|
30
|
+
export * from './server/controllers/index.js';
|
|
31
|
+
export * from './server/types.js';
|
|
32
|
+
export * from './koa.js';
|
|
12
33
|
//# sourceMappingURL=index.js.map
|
package/interceptors.test.js
CHANGED
|
@@ -8,9 +8,8 @@ function h(content) {
|
|
|
8
8
|
}
|
|
9
9
|
let instance;
|
|
10
10
|
let mock;
|
|
11
|
-
import.meta.jest.mock('os');
|
|
12
11
|
beforeEach(() => {
|
|
13
|
-
|
|
12
|
+
vi.useFakeTimers({ now: 0 });
|
|
14
13
|
});
|
|
15
14
|
describe('formData', () => {
|
|
16
15
|
beforeAll(() => {
|
|
@@ -46,7 +45,7 @@ describe('requestLogger', () => {
|
|
|
46
45
|
mock.onAny().reply(200);
|
|
47
46
|
});
|
|
48
47
|
it('should log requests', async () => {
|
|
49
|
-
|
|
48
|
+
vi.spyOn(logger, 'verbose');
|
|
50
49
|
await instance.get('/');
|
|
51
50
|
expect(logger.verbose).toHaveBeenCalledWith(`> 0 ${h('GET / HTTP/1.1')}`);
|
|
52
51
|
});
|
|
@@ -59,15 +58,15 @@ describe('responseLogger', () => {
|
|
|
59
58
|
mock.onAny().reply(200);
|
|
60
59
|
});
|
|
61
60
|
it('should log responses', async () => {
|
|
62
|
-
|
|
61
|
+
vi.spyOn(logger, 'verbose');
|
|
63
62
|
await instance.get('/');
|
|
64
63
|
expect(logger.verbose).toHaveBeenCalledWith(expect.any(String));
|
|
65
64
|
});
|
|
66
65
|
});
|
|
67
66
|
describe('configureAxios', () => {
|
|
68
67
|
beforeEach(() => {
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
vi.spyOn(axios.interceptors.request, 'use').mockImplementation(null);
|
|
69
|
+
vi.spyOn(axios.interceptors.response, 'use').mockImplementation(null);
|
|
71
70
|
});
|
|
72
71
|
it('should set the correct user agent', () => {
|
|
73
72
|
configureAxios('TestClient', '1.2.3');
|
package/jsonschema.d.ts
ADDED
package/jsonschema.js
ADDED
package/koa.d.ts
ADDED
package/koa.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { convertToCsv } from '@appsemble/utils';
|
|
2
|
+
import { bufferParser, jsonParser, bodyParser as koaBodyParser } from 'koas-body-parser';
|
|
3
|
+
import { serializer as koaSerializer } from 'koas-serializer';
|
|
4
|
+
import { csvParser, xWwwFormUrlencodedParser } from './parsers.js';
|
|
5
|
+
function serializer() {
|
|
6
|
+
return koaSerializer({
|
|
7
|
+
serializers: {
|
|
8
|
+
'application/scim+json': (body) => JSON.stringify(body),
|
|
9
|
+
'application/xml': (body) => body,
|
|
10
|
+
'text/csv': convertToCsv,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function bodyParser() {
|
|
15
|
+
return koaBodyParser({
|
|
16
|
+
parsers: {
|
|
17
|
+
'application/scim+json': jsonParser,
|
|
18
|
+
'application/x-www-form-urlencoded': xWwwFormUrlencodedParser,
|
|
19
|
+
'text/csv': csvParser,
|
|
20
|
+
'*/*': bufferParser,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export { bodyParser, serializer };
|
|
25
|
+
export * from 'koas-parameters';
|
|
26
|
+
export * from 'koas-security';
|
|
27
|
+
//# sourceMappingURL=koa.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls Lodash’s mergeWith function
|
|
3
|
+
* but with a comparator function provided to merge any string leaves if they are truthy.
|
|
4
|
+
*
|
|
5
|
+
* @param object The base object to merge.
|
|
6
|
+
* @param otherArgs A list of objects to merge.
|
|
7
|
+
* @returns The resulting merged object.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mergeMessages(object: any, ...otherArgs: any[]): any;
|
package/mergeMessages.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { mergeWith } from 'lodash-es';
|
|
2
|
+
/**
|
|
3
|
+
* Calls Lodash’s mergeWith function
|
|
4
|
+
* but with a comparator function provided to merge any string leaves if they are truthy.
|
|
5
|
+
*
|
|
6
|
+
* @param object The base object to merge.
|
|
7
|
+
* @param otherArgs A list of objects to merge.
|
|
8
|
+
* @returns The resulting merged object.
|
|
9
|
+
*/
|
|
10
|
+
export function mergeMessages(object, ...otherArgs) {
|
|
11
|
+
return mergeWith(object, ...otherArgs, (objectValue, newValue) => {
|
|
12
|
+
if (typeof newValue === 'string') {
|
|
13
|
+
return newValue || objectValue;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=mergeMessages.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SCIMError } from '../scim.js';
|
|
2
|
+
/**
|
|
3
|
+
* Koa middleware for handling Boom errors.
|
|
4
|
+
*
|
|
5
|
+
* @returns Koa middleware that converts Boom errors into an HTTP response.
|
|
6
|
+
*/
|
|
7
|
+
export function boomMiddleware() {
|
|
8
|
+
return async (ctx, next) => {
|
|
9
|
+
try {
|
|
10
|
+
await next();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof SCIMError) {
|
|
14
|
+
ctx.status = Number(error.status);
|
|
15
|
+
ctx.body = error;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const err = error;
|
|
19
|
+
if (!err.isBoom) {
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
const { output } = err;
|
|
23
|
+
ctx.status = output.statusCode;
|
|
24
|
+
ctx.body = output.payload;
|
|
25
|
+
if (err.data) {
|
|
26
|
+
ctx.body.data = err.data;
|
|
27
|
+
}
|
|
28
|
+
ctx.set(output.headers);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=boom.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { notFound, unauthorized } from '@hapi/boom';
|
|
2
|
+
import { request, setTestApp } from 'axios-test-instance';
|
|
3
|
+
import Koa from 'koa';
|
|
4
|
+
import { boomMiddleware } from './boom.js';
|
|
5
|
+
describe('boomMiddleware', () => {
|
|
6
|
+
let app;
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
app = new Koa();
|
|
9
|
+
app.use(boomMiddleware());
|
|
10
|
+
await setTestApp(app);
|
|
11
|
+
});
|
|
12
|
+
it('should catch boom errors', async () => {
|
|
13
|
+
app.use(() => {
|
|
14
|
+
throw notFound('Error not found', "It's nowhere to be seen!");
|
|
15
|
+
});
|
|
16
|
+
const response = await request.get('/');
|
|
17
|
+
expect(response).toMatchObject({
|
|
18
|
+
status: 404,
|
|
19
|
+
data: {
|
|
20
|
+
statusCode: 404,
|
|
21
|
+
error: 'Not Found',
|
|
22
|
+
message: 'Error not found',
|
|
23
|
+
data: "It's nowhere to be seen!",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('should rethrow non-boom errors', async () => {
|
|
28
|
+
app.silent = true;
|
|
29
|
+
const error = new Error('This is a test error');
|
|
30
|
+
let result;
|
|
31
|
+
app.use(() => {
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
app.on('error', (err) => {
|
|
35
|
+
result = err;
|
|
36
|
+
});
|
|
37
|
+
await request.get('/');
|
|
38
|
+
expect(result).toBe(error);
|
|
39
|
+
});
|
|
40
|
+
it('should set Koa headers correctly', async () => {
|
|
41
|
+
app.use(() => {
|
|
42
|
+
throw unauthorized('Not authorized!', ['Basic realm="Access to test data", charset="UTF-8"']);
|
|
43
|
+
});
|
|
44
|
+
const response = await request.get('/');
|
|
45
|
+
expect(response).toMatchObject({
|
|
46
|
+
status: 401,
|
|
47
|
+
headers: expect.objectContaining({
|
|
48
|
+
'www-authenticate': 'Basic realm="Access to test data", charset="UTF-8"',
|
|
49
|
+
}),
|
|
50
|
+
data: {
|
|
51
|
+
statusCode: 401,
|
|
52
|
+
error: 'Unauthorized',
|
|
53
|
+
message: 'Not authorized!',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=boom.test.js.map
|