@appsemble/node-utils 0.33.5 → 0.33.7

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.33.5/config/assets/logo.svg) Appsemble Node Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.33.7/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.33.5/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.33.5)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.33.7/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.33.7)
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.33.5/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.33.7/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
package/app.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { type RemapperContext } from '@appsemble/lang-sdk';
1
2
  import { type App, type AppsembleMessages } from '@appsemble/types';
2
- import { type RemapperContext } from '@appsemble/utils';
3
3
  import { type DefaultContext, type DefaultState, type ParameterizedContext } from 'koa';
4
4
  import { type Options } from './server/types.js';
5
5
  /**
package/app.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
- import { defaultLocale, has, objectCache } from '@appsemble/utils';
4
+ import { defaultLocale } from '@appsemble/lang-sdk';
5
+ import { has, objectCache } from '@appsemble/utils';
5
6
  import { memoize } from '@formatjs/fast-memoize';
6
7
  import { generate, parse, walk } from 'css-tree';
7
8
  import { copy, ensureDir } from 'fs-extra';
@@ -1,4 +1,4 @@
1
- import { type ContainerEnvVar, type ContainerResourceProps, type ContainerResources } from '@appsemble/types';
1
+ import { type ContainerEnvVar, type ContainerResourceProps, type ContainerResources } from '@appsemble/lang-sdk';
2
2
  import { AppsV1Api, CoreV1Api, KubeConfig, type V1EnvVar } from '@kubernetes/client-node';
3
3
  export declare const maxCPU: number;
4
4
  export declare const maxMemoryGi: number;
@@ -1,4 +1,4 @@
1
- import { type CompanionContainerDefinition } from '@appsemble/types';
1
+ import { type CompanionContainerDefinition } from '@appsemble/lang-sdk';
2
2
  /**
3
3
  * Creates or updates secret.
4
4
  * Secret resource is named after the app name + id, where whitespace
@@ -1,4 +1,4 @@
1
- import { type CompanionContainerDefinition } from '@appsemble/types';
1
+ import { type CompanionContainerDefinition } from '@appsemble/lang-sdk';
2
2
  import { type V1Deployment, type V1Service } from '@kubernetes/client-node';
3
3
  /**
4
4
  * Create the spec objects for a service, deployment, and pod.
@@ -1,5 +1,6 @@
1
+ import { defaultLocale, normalizeBlockName } from '@appsemble/lang-sdk';
1
2
  import { getAppsembleMessages, mergeMessages, } from '@appsemble/node-utils';
2
- import { defaultLocale, extractAppMessages, normalizeBlockName, } from '@appsemble/utils';
3
+ import { extractAppMessages } from '@appsemble/utils';
3
4
  import tags from 'language-tags';
4
5
  import { assertKoaCondition, throwKoaError } from './koa.js';
5
6
  export async function getMessagesUtil(ctx, language, appId, merge, { getApp, getAppMessages, getBlockMessages }, override = 'true') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/node-utils",
3
- "version": "0.33.5",
3
+ "version": "0.33.7",
4
4
  "description": "NodeJS utilities used by Appsemble internally.",
5
5
  "keywords": [
6
6
  "app",
@@ -40,8 +40,9 @@
40
40
  "test": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "@appsemble/types": "0.33.5",
44
- "@appsemble/utils": "0.33.5",
43
+ "@appsemble/types": "0.33.7",
44
+ "@appsemble/utils": "0.33.7",
45
+ "@appsemble/lang-sdk": "0.33.7",
45
46
  "@formatjs/fast-memoize": "^2.0.0",
46
47
  "@fortawesome/fontawesome-free": "^6.0.0",
47
48
  "@kubernetes/client-node": "^0.22.2",
package/resource.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type AppDefinition, type ResourceDefinition, type Resource as ResourceType } from '@appsemble/types';
1
+ import { type AppDefinition, type ResourceDefinition } from '@appsemble/lang-sdk';
2
+ import { type Resource as ResourceType } from '@appsemble/types';
2
3
  import { type PreValidatePropertyFunction } from 'jsonschema';
3
4
  import { type Context, type DefaultContext, type DefaultState, type ParameterizedContext } from 'koa';
4
5
  import { type JsonValue } from 'type-fest';
package/resource.js CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mapValues } from '@appsemble/utils';
3
3
  import { addMilliseconds, isPast, parseISO } from 'date-fns';
4
4
  import { ValidationError, Validator } from 'jsonschema';
5
+ import { partition } from 'lodash-es';
5
6
  import parseDuration from 'parse-duration';
6
7
  import { preProcessCSV } from './csv.js';
7
8
  import { handleValidatorResult, TempFile } from './index.js';
@@ -118,6 +119,7 @@ export function extractResourceBody(ctx) {
118
119
  preValidateProperty,
119
120
  ];
120
121
  }
122
+ // XXX: unify with webhooks logic
121
123
  /**
122
124
  * Process an incoming resource request body.
123
125
  *
@@ -138,38 +140,41 @@ export function extractResourceBody(ctx) {
138
140
  export function processResourceBody(ctx, definition, knownAssetIds = [], knownExpires, knownAssetNameIds = [], isPatch = false) {
139
141
  const [resource, assets, preValidateProperty] = extractResourceBody(ctx);
140
142
  const validator = new Validator();
141
- const assetIdMap = new Map();
142
- const assetUsedMap = new Map();
143
143
  const reusedAssets = new Set();
144
- const thumbnailAssets = [];
145
- const regularAssets = [];
144
+ const usedAssetIndices = new Set();
146
145
  const thumbnailAssetSuffix = '-thumbnail.png';
147
- for (const asset of assets) {
148
- if (asset.filename?.endsWith(thumbnailAssetSuffix)) {
149
- thumbnailAssets.push(asset);
150
- }
151
- else {
152
- regularAssets.push(asset);
153
- }
146
+ const [thumbnailAssets, regularAssets] = partition(
147
+ // Preserve original index to handle asset references in data
148
+ assets.map((asset, index) => ({ asset, index })), ({ asset }) => asset.filename?.endsWith(thumbnailAssetSuffix));
149
+ const preparedRegularAssets = regularAssets.map(({ asset: { filename, mime, path }, index }) => ({
150
+ index,
151
+ asset: {
152
+ id: randomUUID(),
153
+ filename,
154
+ mime,
155
+ path,
156
+ },
157
+ }));
158
+ // Maps 1 unique thumbnail asset to 1 unique regular asset (without other thumbnail assets
159
+ // pointing to it), or a random UUID if no match is found
160
+ // eslint-disable-next-line unicorn/no-array-reduce
161
+ const { prepared: preparedThumbnailAssets } = thumbnailAssets.reduce(({ prepared, used }, { asset, index }) => {
162
+ const reg = preparedRegularAssets.find((regular) => regular.asset.filename?.startsWith(asset.filename.replace(thumbnailAssetSuffix, '')) &&
163
+ !used.includes(regular.asset.id));
164
+ const id = reg ? `${reg.asset.id}-thumbnail` : randomUUID();
165
+ return {
166
+ prepared: [...prepared, { index, asset: { ...asset, id } }],
167
+ used: used.concat(reg?.asset.id ?? []),
168
+ };
169
+ }, { used: [], prepared: [] });
170
+ const assetIndexIdMap = new Map();
171
+ for (const { asset, index } of preparedRegularAssets) {
172
+ assetIndexIdMap.set(index, asset.id);
154
173
  }
155
- const preparedRegularAssets = regularAssets.map(({ filename, mime, path }, index) => {
156
- const id = randomUUID();
157
- assetIdMap.set(index, id);
158
- assetUsedMap.set(index, false);
159
- return { path, filename, id, mime };
160
- });
161
- const usedPreparedRegularAssets = [];
162
- const preparedThumbnailAssets = thumbnailAssets.map(({ filename, mime, path }, index) => {
163
- const regularAsset = preparedRegularAssets.find((ra) => !usedPreparedRegularAssets.includes(ra.id) &&
164
- ra.filename?.startsWith(filename.replace(thumbnailAssetSuffix, '')));
165
- const id = regularAsset ? `${regularAsset.id}-thumbnail` : randomUUID();
166
- // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
167
- usedPreparedRegularAssets.push(regularAsset?.id);
168
- assetIdMap.set(index + preparedRegularAssets.length, id);
169
- assetUsedMap.set(index, false);
170
- return { path, filename, id, mime };
171
- });
172
- const preparedAssets = [...preparedRegularAssets, ...preparedThumbnailAssets];
174
+ for (const { asset, index } of preparedThumbnailAssets) {
175
+ assetIndexIdMap.set(index, asset.id);
176
+ }
177
+ const preparedAssets = [...preparedRegularAssets, ...preparedThumbnailAssets].map(({ asset }) => asset);
173
178
  validator.customFormats.binary = (input) => {
174
179
  if (knownAssetIds.includes(input)) {
175
180
  reusedAssets.add(input);
@@ -184,7 +189,7 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
184
189
  return false;
185
190
  }
186
191
  const num = Number(input);
187
- if (assetUsedMap.get(num)) {
192
+ if (usedAssetIndices.has(num)) {
188
193
  return false;
189
194
  }
190
195
  return num >= 0 && num < assets.length;
@@ -200,7 +205,9 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
200
205
  { type: 'string', format: 'date-time' },
201
206
  {
202
207
  type: 'string',
203
- 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))?$/
208
+ pattern:
209
+ // TODO: This pattern is duplicated at least 5 times across the codebase
210
+ /^(\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))?$/
204
211
  .source,
205
212
  },
206
213
  ],
@@ -270,24 +277,22 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
270
277
  return value;
271
278
  }
272
279
  const num = Number(value);
273
- if (!assetIdMap.has(num)) {
280
+ if (!assetIndexIdMap.has(num)) {
274
281
  return value;
275
282
  }
276
- const uuid = assetIdMap.get(num);
283
+ const uuid = assetIndexIdMap.get(num);
277
284
  const currentResource = Array.isArray(resource) ? resource[path[0]] : resource;
278
285
  const preparedAsset = preparedAssets.find((asset) => asset.id === uuid);
279
286
  if (preparedAsset) {
280
287
  preparedAsset.resource = currentResource;
281
288
  }
282
- assetUsedMap.set(num, true);
289
+ usedAssetIndices.add(num);
283
290
  return uuid;
284
291
  },
285
292
  });
286
293
  result.errors.push(...customErrors);
287
- for (const [assetId, used] of assetUsedMap.entries()) {
288
- if (!used) {
289
- result.errors.push(new ValidationError('is not referenced from the resource', assetId, undefined, ['assets', assetId], 'binary', 'format'));
290
- }
294
+ for (const index of [...assetIndexIdMap.keys()].filter((idx) => !usedAssetIndices.has(idx))) {
295
+ result.errors.push(new ValidationError('is not referenced from the resource', index, undefined, ['assets', index], 'binary', 'format'));
291
296
  }
292
297
  handleValidatorResult(ctx, result, 'Resource validation failed');
293
298
  return [resource, preparedAssets, knownAssetIds.filter((id) => !reusedAssets.has(id))];
@@ -1,4 +1,4 @@
1
- import { AppPermission } from '@appsemble/types';
1
+ import { AppPermission } from '@appsemble/lang-sdk';
2
2
  import { assertKoaCondition } from '../../../../../koa.js';
3
3
  export function createGetAppGroupsController({ checkAuthSubjectAppPermissions, getApp, getAppGroups, }) {
4
4
  return async (ctx) => {
@@ -1,5 +1,5 @@
1
+ import { defaultLocale, remap } from '@appsemble/lang-sdk';
1
2
  import { assertKoaCondition, getRemapperContext, getResourceDefinition, } from '@appsemble/node-utils';
2
- import { defaultLocale, remap } from '@appsemble/utils';
3
3
  export function createGetAppResourceByIdController(options) {
4
4
  return async (ctx) => {
5
5
  const { pathParams: { appId, resourceId, resourceType }, queryParams: { selectedGroupId, view }, user: authSubject, } = ctx;
@@ -1,5 +1,5 @@
1
+ import { defaultLocale, remap } from '@appsemble/lang-sdk';
1
2
  import { getRemapperContext, getResourceDefinition, } from '@appsemble/node-utils';
2
- import { defaultLocale, remap } from '@appsemble/utils';
3
3
  import { generateResourceQuery } from '../../../../utils/resources.js';
4
4
  export function createQueryAppResourcesController(options) {
5
5
  return async (ctx) => {
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { baseTheme } from '@appsemble/lang-sdk';
2
3
  import { bulmaVersion, logger } from '@appsemble/node-utils';
3
- import { baseTheme } from '@appsemble/utils';
4
4
  import { renderSync } from 'sass';
5
5
  import stripBom from 'strip-bom';
6
6
  const require = createRequire(import.meta.url);
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { createThemeURL, defaultLocale, getAppBlocks, mergeThemes } from '@appsemble/utils';
2
+ import { getAppBlocks } from '@appsemble/lang-sdk';
3
+ import { createThemeURL, defaultLocale, mergeThemes } from '@appsemble/utils';
3
4
  import { organizationBlocklist } from '../../../organizationBlocklist.js';
4
5
  import { makeCSP, render } from '../../../render.js';
5
6
  import { bulmaVersion, faVersion } from '../../../versions.js';
@@ -1,5 +1,6 @@
1
+ import { baseTheme } from '@appsemble/lang-sdk';
1
2
  import { assertKoaCondition } from '@appsemble/node-utils';
2
- import { baseTheme, normalize } from '@appsemble/utils';
3
+ import { normalize } from '@appsemble/utils';
3
4
  import { extension } from 'mime-types';
4
5
  const iconSizes = [48, 144, 192, 512];
5
6
  export function createManifestHandler({ getApp, getAppScreenshots }) {
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { getAppBlocks } from '@appsemble/lang-sdk';
2
3
  import { assertKoaCondition } from '@appsemble/node-utils';
3
- import { getAppBlocks } from '@appsemble/utils';
4
4
  export function createServiceWorkerHandler({ getApp, getBlocksAssetsPaths }) {
5
5
  return async (ctx) => {
6
6
  const production = process.env.NODE_ENV === 'production';
package/server/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Readable } from 'node:stream';
2
- import { type App, type AppConfigEntry, type AppMemberInfo, type AppMessages, type Asset, type BlockDefinition, type BlockManifest, type ControllerDefinition, type CustomAppPermission, type EmailActionDefinition, type Group, type OrganizationPermission, type Resource, type ResourceDefinition, type Theme as ThemeType } from '@appsemble/types';
3
- import { type IdentifiableBlock } from '@appsemble/utils';
2
+ import { type BlockDefinition, type ControllerDefinition, type CustomAppPermission, type EmailActionDefinition, type IdentifiableBlock, type ResourceDefinition, type Theme as ThemeType } from '@appsemble/lang-sdk';
3
+ import { type App, type AppConfigEntry, type AppMemberInfo, type AppMessages, type Asset, type BlockManifest, type Group, type OrganizationPermission, type Resource } from '@appsemble/types';
4
4
  import { type RawAxiosRequestConfig } from 'axios';
5
5
  import { type DefaultContext as DefaultContextInterface, type DefaultState, type ParameterizedContext } from 'koa';
6
6
  export interface AuthSubject {
@@ -1,5 +1,6 @@
1
+ import { type EmailActionDefinition, type NotifyActionDefinition } from '@appsemble/lang-sdk';
1
2
  import { type Options } from '@appsemble/node-utils';
2
- import { type App, type EmailActionDefinition, type NotifyActionDefinition } from '@appsemble/types';
3
+ import { type App } from '@appsemble/types';
3
4
  import { type Context, type Middleware } from 'koa';
4
5
  export declare function handleEmail(ctx: Context, app: App, action: EmailActionDefinition, options: Options): Promise<void>;
5
6
  export declare function handleNotify(ctx: Context, app: App, action: NotifyActionDefinition, options: Options): Promise<void>;
@@ -1,5 +1,5 @@
1
+ import { defaultLocale, remap, } from '@appsemble/lang-sdk';
1
2
  import { assertKoaCondition, createFormData, EmailQuotaExceededError, getContainerNamespace, getRemapperContext, logger, parseServiceUrl, scaleDeployment, setLastRequestAnnotation, throwKoaError, version, waitForPodReadiness, } from '@appsemble/node-utils';
2
- import { defaultLocale, remap } from '@appsemble/utils';
3
3
  import axios from 'axios';
4
4
  import { get, mapValues, pick } from 'lodash-es';
5
5
  /**
@@ -1,4 +1,5 @@
1
- import { type App, type ResourceDefinition } from '@appsemble/types';
1
+ import { type ResourceDefinition } from '@appsemble/lang-sdk';
2
+ import { type App } from '@appsemble/types';
2
3
  import { type Context } from 'koa';
3
4
  import { type Options, type OrderItem, type WhereOptions } from '../types.js';
4
5
  export declare function generateResourceQuery(ctx: Context, { parseQuery }: Options, resourceDefinition?: ResourceDefinition): {