@appsemble/lang-sdk 0.36.10-test.0 → 0.36.10-test.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.36.10-test.0",
3
+ "version": "0.36.10-test.1",
4
4
  "description": "Language SDK for Appsemble",
5
5
  "keywords": [
6
6
  "app",
@@ -82,6 +82,17 @@ name of the resource and how it should behave.
82
82
  },
83
83
  },
84
84
  },
85
+ unique: {
86
+ type: 'array',
87
+ minItems: 1,
88
+ uniqueItems: true,
89
+ items: {
90
+ oneOf: [
91
+ { type: 'string' },
92
+ { type: 'array', items: { type: 'string' }, minItems: 2, uniqueItems: true },
93
+ ],
94
+ },
95
+ },
85
96
  positioning: {
86
97
  type: 'boolean',
87
98
  default: false,
@@ -1,9 +1,11 @@
1
1
  import { type OpenAPIV3 } from 'openapi-types';
2
2
  import { type HTTPMethods } from './http.js';
3
3
  import { type Remapper } from './remapper.js';
4
+ export declare function inferResourceUniqueFieldType(propertySchema: OpenAPIV3.SchemaObject | undefined): string;
4
5
  export type ResourceViewAction = 'get' | 'query';
5
6
  export type OwnResourceAction = ResourceViewAction | 'delete' | 'patch' | 'update';
6
7
  export type ResourceAction = ResourceViewAction | 'create' | 'delete' | 'history.get' | 'patch' | 'update.positions' | 'update';
8
+ export type ResourceUniqueConstraint = string | [string, string, ...string[]];
7
9
  export interface ResourceDefinition {
8
10
  /**
9
11
  * A definition of how versioning should happen for instances of this resource.
@@ -70,6 +72,13 @@ export interface ResourceDefinition {
70
72
  * The references this resources has to other resources.
71
73
  */
72
74
  references?: Record<string, ResourceReference>;
75
+ /**
76
+ * The unique constraints that should be enforced across all instances of this resource.
77
+ *
78
+ * Each constraint can be a single field name or an array of field names for a composite unique
79
+ * key.
80
+ */
81
+ unique?: ResourceUniqueConstraint[];
73
82
  /**
74
83
  * A time string representing when a resource should expire.
75
84
  *
package/types/resource.js CHANGED
@@ -1,2 +1,22 @@
1
- export {};
1
+ const supportedResourceUniqueFieldTypes = new Set(['boolean', 'integer', 'number', 'string']);
2
+ export function inferResourceUniqueFieldType(propertySchema) {
3
+ if (propertySchema?.type && supportedResourceUniqueFieldTypes.has(propertySchema.type)) {
4
+ return propertySchema.type;
5
+ }
6
+ const enumValues = propertySchema?.enum;
7
+ if (!Array.isArray(enumValues) || !enumValues.length) {
8
+ return '';
9
+ }
10
+ const types = new Set(enumValues.map((value) => typeof value));
11
+ if (types.size !== 1) {
12
+ return '';
13
+ }
14
+ if (types.has('string') || types.has('boolean')) {
15
+ return typeof enumValues[0];
16
+ }
17
+ if (types.has('number')) {
18
+ return enumValues.every(Number.isInteger) ? 'integer' : 'number';
19
+ }
20
+ return '';
21
+ }
2
22
  //# sourceMappingURL=resource.js.map
package/validation.js CHANGED
@@ -2,13 +2,15 @@ import cronParser from 'cron-parser';
2
2
  import { ValidationError, Validator } from 'jsonschema';
3
3
  import languageTags from 'language-tags';
4
4
  import { getAppInheritedRoles, getAppPossibleGuestPermissions, getAppPossiblePermissions, getAppRolePermissions, } from './authorization.js';
5
+ import { BlockParamInstanceValidator } from './BasicValidator.js';
5
6
  import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
7
+ import { partialNormalized } from './constants/index.js';
6
8
  import { findPageByName } from './findPageByName.js';
7
- import { BlockParamInstanceValidator, normalize, partialNormalized } from './index.js';
8
9
  import { iterApp } from './iterApp.js';
9
10
  import { has } from './miscellaneous.js';
11
+ import { normalize } from './normalize.js';
10
12
  import { serverActions } from './serverActions.js';
11
- import { PredefinedAppRole, predefinedAppRolePermissions, } from './types/index.js';
13
+ import { PredefinedAppRole, predefinedAppRolePermissions, inferResourceUniqueFieldType, } from './types/index.js';
12
14
  const allResourcePermissionPattern = /^\$resource:all:(get|history:get|query|create|delete|patch|update)$/;
13
15
  const resourcePermissionPattern = /^\$resource:[^:]+:(get|history:get|query|create|delete|patch|update)$/;
14
16
  const allOwnResourcePermissionPattern = /^\$resource:all:own:(get|query|delete|patch|update)$/;
@@ -129,6 +131,7 @@ function validateResourceSchemas(definition, report) {
129
131
  continue;
130
132
  }
131
133
  const { enforceOrderingGroupByFields, positioning, schema } = resource;
134
+ const resourcePrefix = ['resources', resourceName];
132
135
  const prefix = ['resources', resourceName, 'schema'];
133
136
  if (!positioning && enforceOrderingGroupByFields?.length) {
134
137
  report(enforceOrderingGroupByFields, 'must set positioning to true', [
@@ -155,6 +158,7 @@ function validateResourceSchemas(definition, report) {
155
158
  'expires',
156
159
  // XXX: is position reserved?
157
160
  ]);
161
+ const uniqueReservedKeywords = new Set([...reservedKeywords, 'id', '$created', '$updated']);
158
162
  if (reservedKeywords.has(resourceName)) {
159
163
  report(schema, 'is a reserved keyword', ['resources', resourceName]);
160
164
  }
@@ -231,6 +235,38 @@ function validateResourceSchemas(definition, report) {
231
235
  }
232
236
  }
233
237
  }
238
+ if ('unique' in resource && resource.unique !== undefined) {
239
+ const propertyNames = new Set(Object.keys(schema.properties ?? {}));
240
+ const seenConstraints = new Set();
241
+ for (const [constraintId, constraint] of resource.unique.entries()) {
242
+ const fields = [].concat(constraint);
243
+ const normalizedConstraint = fields.toSorted().join('\0');
244
+ if (seenConstraints.has(normalizedConstraint)) {
245
+ report(constraint, 'duplicates another unique constraint', [
246
+ ...resourcePrefix,
247
+ 'unique',
248
+ constraintId,
249
+ ]);
250
+ }
251
+ seenConstraints.add(normalizedConstraint);
252
+ for (const [fieldId, field] of fields.entries()) {
253
+ const path = Array.isArray(constraint)
254
+ ? [...resourcePrefix, 'unique', constraintId, fieldId]
255
+ : [...resourcePrefix, 'unique', constraintId];
256
+ if (uniqueReservedKeywords.has(field)) {
257
+ report(field, `unique field cannot be a reserved keyword: ${field}`, path);
258
+ }
259
+ if (!propertyNames.has(field)) {
260
+ report(field, `unique field must be defined in properties: ${field}`, path);
261
+ }
262
+ if (propertyNames.has(field) &&
263
+ !uniqueReservedKeywords.has(field) &&
264
+ !inferResourceUniqueFieldType(schema.properties?.[field])) {
265
+ report(field, `unique field must have type string, integer, number, boolean, or enum: ${field}`, path);
266
+ }
267
+ }
268
+ }
269
+ }
234
270
  }
235
271
  }
236
272
  function validateController(definition, controllerImplementations, report) {
@@ -416,8 +452,8 @@ function validateGridLayout(definition, report) {
416
452
  if (page.type === 'tabs') {
417
453
  const tabsPage = page;
418
454
  if (tabsPage.tabs) {
419
- for (const [tabIdx, tab] of tabsPage.tabs.entries()) {
420
- validateSubPageLayout(tab.layout, tab.blocks, report, [...path, 'tabs', tabIdx]);
455
+ for (const [tabId, tab] of tabsPage.tabs.entries()) {
456
+ validateSubPageLayout(tab.layout, tab.blocks, report, [...path, 'tabs', tabId]);
421
457
  }
422
458
  }
423
459
  else if (tabsPage.definition?.foreach) {
@@ -428,8 +464,8 @@ function validateGridLayout(definition, report) {
428
464
  // Flow page
429
465
  if (page.type === 'flow') {
430
466
  const flowPage = page;
431
- for (const [stepIdx, step] of flowPage.steps.entries()) {
432
- validateSubPageLayout(step.layout, step.blocks, report, [...path, 'steps', stepIdx]);
467
+ for (const [stepId, step] of flowPage.steps.entries()) {
468
+ validateSubPageLayout(step.layout, step.blocks, report, [...path, 'steps', stepId]);
433
469
  }
434
470
  return;
435
471
  }