@appsemble/node-utils 0.37.0 → 0.37.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 CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.37.0/config/assets/logo.svg) Appsemble Node Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.37.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.37.0/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.37.0)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.37.1/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.37.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.37.0/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.1/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/node-utils",
3
- "version": "0.37.0",
3
+ "version": "0.37.1",
4
4
  "description": "NodeJS utilities used by Appsemble internally.",
5
5
  "keywords": [
6
6
  "app",
@@ -40,9 +40,9 @@
40
40
  "test": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "@appsemble/lang-sdk": "0.37.0",
44
- "@appsemble/types": "0.37.0",
45
- "@appsemble/utils": "0.37.0",
43
+ "@appsemble/lang-sdk": "0.37.1",
44
+ "@appsemble/types": "0.37.1",
45
+ "@appsemble/utils": "0.37.1",
46
46
  "@formatjs/fast-memoize": "^2.0.0",
47
47
  "@fortawesome/fontawesome-free": "^6.0.0",
48
48
  "@inquirer/prompts": "^8.0.0",
package/resource.d.ts CHANGED
@@ -17,6 +17,11 @@ export declare function serializeServerResource(data: any): JsonValue | {
17
17
  assets: TempFile[];
18
18
  };
19
19
  export type SerializedServerResourceBody = ReturnType<typeof serializeServerResource>;
20
+ export type SerializedMultipartBody = Extract<SerializedServerResourceBody, {
21
+ resource: JsonValue;
22
+ assets: TempFile[];
23
+ }>;
24
+ export declare function isSerializedMultipartBody(value: SerializedServerResourceBody): value is SerializedMultipartBody;
20
25
  /**
21
26
  * Get the resource definition of an app by name.
22
27
  *
package/resource.js CHANGED
@@ -44,7 +44,7 @@ export function serializeServerResource(data) {
44
44
  assets,
45
45
  };
46
46
  }
47
- function isSerializedMultipartBody(value) {
47
+ export function isSerializedMultipartBody(value) {
48
48
  return (value != null &&
49
49
  typeof value === 'object' &&
50
50
  !Array.isArray(value) &&
@@ -2,4 +2,4 @@ import { type Middleware } from 'koa';
2
2
  import { type Options } from '../../types.js';
3
3
  export declare const bulmaURL: string;
4
4
  export declare const faURL: string;
5
- export declare function createIndexHandler({ createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, }: Options): Middleware;
5
+ export declare function createIndexHandler(options: Options): Middleware;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { getAppBlocks } from '@appsemble/lang-sdk';
3
3
  import { createThemeURL, defaultLocale, mergeThemes } from '@appsemble/utils';
4
4
  import { organizationBlocklist } from '../../../organizationBlocklist.js';
@@ -6,8 +6,50 @@ import { makeCSP, render } from '../../../render.js';
6
6
  import { bulmaVersion, faVersion } from '../../../versions.js';
7
7
  export const bulmaURL = `/bulma/${bulmaVersion}/bulma.min.css`;
8
8
  export const faURL = `/fa/${faVersion}/css/all.min.css`;
9
- export function createIndexHandler({ createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, }) {
9
+ function hash(value) {
10
+ return createHash('sha256').update(JSON.stringify(value)).digest('base64url');
11
+ }
12
+ function getAppUpdated(app) {
13
+ return app.$updated ?? '';
14
+ }
15
+ function getAppSnapshotId(app) {
16
+ return app.version ?? 'none';
17
+ }
18
+ function getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages, }) {
19
+ const blocks = identifiableBlocks
20
+ .map(({ type, version }) => ({ type, version }))
21
+ .sort((a, b) => a.type.localeCompare(b.type) || a.version.localeCompare(b.version));
22
+ return [
23
+ 'app-settings',
24
+ app.id,
25
+ getAppUpdated(app),
26
+ getAppSnapshotId(app),
27
+ hash({ host, hostname }),
28
+ hash(languages),
29
+ hash(blocks),
30
+ ].join(':');
31
+ }
32
+ function getMessagesCacheKey({ app }) {
33
+ return ['app-messages', app.id, getAppUpdated(app)].join(':');
34
+ }
35
+ async function getCachedValue(cache, key, fallback) {
36
+ if (!cache) {
37
+ return [await fallback(), 'disabled'];
38
+ }
39
+ const cached = await cache.get(key);
40
+ if (cached.status === 'hit') {
41
+ return [cached.value, 'hit'];
42
+ }
43
+ const value = await fallback();
44
+ if (cached.status === 'miss') {
45
+ const setStatus = await cache.set(key, value);
46
+ return [value, setStatus === 'error' ? 'error' : 'miss'];
47
+ }
48
+ return [value, cached.status];
49
+ }
50
+ export function createIndexHandler(options) {
10
51
  return async (ctx) => {
52
+ const { appServingCache, createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, } = options;
11
53
  const { hostname, path } = ctx;
12
54
  const host = getHost({ context: ctx });
13
55
  // Prevent mime-type sniffing,
@@ -40,21 +82,26 @@ export function createIndexHandler({ createSettings, getApp, getAppDetails, getA
40
82
  return;
41
83
  }
42
84
  const defaultLanguage = app.definition.defaultLanguage || defaultLocale;
43
- const appMessages = await getAppMessages({ app, context: ctx });
85
+ const [appMessages, messagesCacheStatus] = await getCachedValue(appServingCache, getMessagesCacheKey({ app }), () => getAppMessages({ app, context: ctx }));
86
+ ctx.set('X-Appsemble-Messages-Cache', messagesCacheStatus);
44
87
  const languages = [
45
88
  ...new Set([...appMessages.map(({ language }) => language), defaultLanguage]),
46
89
  ].sort();
47
90
  const identifiableBlocks = getAppBlocks(app.definition);
48
91
  const nonce = randomBytes(16).toString('base64');
49
- const [settingsHash, settings] = await createSettings({
50
- context: ctx,
51
- app,
52
- host,
53
- hostname,
54
- identifiableBlocks,
55
- languages,
56
- nonce,
92
+ const [{ settingsHash, settings }, settingsCacheStatus] = await getCachedValue(appServingCache, getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages }), async () => {
93
+ const [digest, script] = await createSettings({
94
+ context: ctx,
95
+ app,
96
+ host,
97
+ hostname,
98
+ identifiableBlocks,
99
+ languages,
100
+ nonce,
101
+ });
102
+ return { settingsHash: digest, settings: script };
57
103
  });
104
+ ctx.set('X-Appsemble-Settings-Cache', settingsCacheStatus);
58
105
  const csp = getCsp({ app, settingsHash, hostname, host, nonce });
59
106
  ctx.set('Content-Security-Policy', makeCSP(csp));
60
107
  const updated = app.$updated ? new Date(app.$updated) : new Date();
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  export function createServiceWorkerHandler() {
3
4
  return async (ctx) => {
@@ -6,8 +7,18 @@ export function createServiceWorkerHandler() {
6
7
  const serviceWorker = await (production
7
8
  ? readFile(new URL('../../../../../dist/app/service-worker.js', import.meta.url), 'utf8')
8
9
  : ctx.fs.promises.readFile(filename, 'utf8'));
9
- ctx.body = serviceWorker;
10
10
  ctx.type = 'application/javascript';
11
+ // Browsers bypass their HTTP cache when checking for service worker updates, so a strong ETag
12
+ // lets them revalidate with a 304 instead of downloading the full script on every check.
13
+ ctx.set('etag', `"${createHash('sha256').update(serviceWorker).digest('base64url')}"`);
14
+ ctx.set('cache-control', 'no-cache');
15
+ ctx.status = 200;
16
+ if (ctx.fresh) {
17
+ ctx.status = 304;
18
+ }
19
+ else {
20
+ ctx.body = serviceWorker;
21
+ }
11
22
  };
12
23
  }
13
24
  //# sourceMappingURL=serviceWorkerHandler.js.map
package/server/types.d.ts CHANGED
@@ -404,6 +404,15 @@ export interface ParsedQuery {
404
404
  where: WhereOptions;
405
405
  }
406
406
  export type ContentSecurityPolicy = Record<string, (string | false)[]>;
407
+ export type AppServingCacheStatus = 'disabled' | 'error' | 'hit' | 'miss';
408
+ export interface AppServingCacheResult<T> {
409
+ status: AppServingCacheStatus;
410
+ value?: T;
411
+ }
412
+ export interface AppServingCache {
413
+ get: <T>(key: string) => Promise<AppServingCacheResult<T>>;
414
+ set: <T>(key: string, value: T) => Promise<AppServingCacheStatus>;
415
+ }
407
416
  export interface Options {
408
417
  getSecurityEmail: () => string;
409
418
  getCurrentAppMember: (params: GetCurrentAppMemberParams) => Promise<AppMemberInfo | null>;
@@ -423,11 +432,12 @@ export interface Options {
423
432
  getBlockMessages: (params: GetBlockMessagesParams) => Promise<BlockMessages[]>;
424
433
  getBlockAsset: (params: GetBlockAssetParams) => Promise<ProjectAsset>;
425
434
  getBlocksAssetsPaths: (params: GetBlocksAssetsPathsParams) => Promise<string[]>;
426
- getTheme: (params: GetThemeParams) => Promise<Theme>;
435
+ getTheme: (params: GetThemeParams) => Promise<Theme | null>;
427
436
  createTheme: (params: CreateThemeParams) => Promise<Theme>;
428
437
  getHost: (params: GetHostParams) => string;
429
438
  getCsp: (params: GetCspParams) => ContentSecurityPolicy;
430
439
  createSettings: (params: CreateSettingsParams) => Promise<[digest: string, script: string]>;
440
+ appServingCache?: AppServingCache;
431
441
  applyAppServiceSecrets: (params: ApplyAppServiceSecretsParams) => Promise<RawAxiosRequestConfig<any>>;
432
442
  checkAppMemberAppPermissions: (params: CheckAppMemberAppPermissionsParams) => Promise<void>;
433
443
  checkUserOrganizationPermissions: (params: CheckUserOrganizationPermissionsParams) => Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { defaultLocale, remap, } from '@appsemble/lang-sdk';
2
2
  import { assertKoaCondition, createFormData, EmailQuotaExceededError, getContainerNamespace, getRemapperContext, getSSRFProtectedAgents, logger, parseServiceUrl, scaleDeployment, setLastRequestAnnotation, throwKoaError, version, waitForPodReadiness, } from '@appsemble/node-utils';
3
+ import { deserializeResource } from '@appsemble/utils';
3
4
  import axios from 'axios';
4
- import { get, mapValues, pick } from 'lodash-es';
5
+ import { get, pick } from 'lodash-es';
5
6
  /**
6
7
  * These response headers are forwarded when proxying requests.
7
8
  */
@@ -40,26 +41,6 @@ export async function handleNotify(ctx, app, action, options) {
40
41
  await sendNotifications({ app, to, title, body, link });
41
42
  ctx.status = 204;
42
43
  }
43
- function deserializeResource(data) {
44
- // Extract the resource and assets from the JSON object
45
- const { resource } = data;
46
- const assets = data.assets;
47
- // Function to replace asset placeholders with actual Blobs
48
- const replaceAssets = (value) => {
49
- if (Array.isArray(value)) {
50
- return value.map(replaceAssets);
51
- }
52
- if (typeof value === 'string' && /^\d+$/.test(value)) {
53
- return assets[Number(value)];
54
- }
55
- if (value && typeof value === 'object') {
56
- return mapValues(value, replaceAssets);
57
- }
58
- return value;
59
- };
60
- // Replace placeholders and return the deserialized resource
61
- return replaceAssets(resource);
62
- }
63
44
  async function handleRequestProxy(ctx, app, action, useBody, options) {
64
45
  const { method, query, request: { body, headers }, } = ctx;
65
46
  let data;