openxiangda-contracts 2.16.1 → 2.17.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/dist/native-compiler/compiler.d.ts +1 -0
- package/dist/native-compiler/compiler.d.ts.map +1 -1
- package/dist/native-compiler/compiler.js +339 -3
- package/dist/native-compiler/compiler.js.map +1 -1
- package/dist/native-compiler/index.cjs +4 -4
- package/dist/native-compiler/index.js +4 -4
- package/dist/native.d.ts +134 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/schemas.d.ts +705 -79
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +147 -0
- package/dist/schemas.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ declare const COMPILER_CONTRACT_VERSION: "native-4";
|
|
|
4
4
|
export declare const NATIVE_CONTRACT_CAPACITY_V2: Readonly<{
|
|
5
5
|
readonly perspectives: 100;
|
|
6
6
|
readonly resources: 100;
|
|
7
|
+
readonly subjectReadSurfaces: 50;
|
|
7
8
|
readonly capabilities: 2000;
|
|
8
9
|
readonly operations: 500;
|
|
9
10
|
readonly eventConsumers: 100;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../src/native-compiler/compiler.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,kCAAkC,IAAI,uCAAuC,EACnF,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../src/native-compiler/compiler.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,kCAAkC,IAAI,uCAAuC,EACnF,MAAM,aAAa,CAAC;AAyBrB,QAAA,MAAM,iBAAiB,mDAAmD,CAAC;AAC3E,QAAA,MAAM,yBAAyB,YAA2C,CAAC;AAgF3E,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;EAe7B,CAAC;AACZ,eAAO,MAAM,mBAAmB,6HAItB,CAAC;AACX,eAAO,MAAM,uBAAuB,u/CAkC1B,CAAC;AAGX,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEtC,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IAC1C,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IAC5C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,6BAA6B,CAAC,CAAC,GAAG,UAAU;IAC3D,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iCAAiC;IAChD,aAAa,EAAE,OAAO,iBAAiB,CAAC;IACxC,uBAAuB,EAAE,OAAO,yBAAyB,CAAC;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,WAAW,EAAE;QACX,KAAK,EAAE,6BAA6B,CAAC;QACrC,IAAI,EAAE,6BAA6B,CAAC;QACpC,MAAM,EAAE,6BAA6B,CAAC;QACtC,SAAS,EAAE,6BAA6B,CAAC;QACzC,OAAO,EAAE,6BAA6B,CAAC;QACvC,SAAS,EAAE,6BAA6B,CAAC;KAC1C,CAAC;IACF,4BAA4B,EAAE,uCAAuC,EAAE,CAAC;IACxE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,gCAAiC,SAAQ,KAAK;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAG3C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAY5C;AAED,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,gCAAgC,GACtC,iCAAiC,CAkJnC;AAED;;;GAGG;AACH,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,UAAU,GACjB,uCAAuC,EAAE,CA4R3C;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE;IACtD,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,aAAa,EAAE,UAAU,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,UAAU,EAAE,CAAC;CAChC,GAAG,wBAAwB,EAAE,CA4J7B"}
|
|
@@ -7,6 +7,7 @@ import { OPENXIANGDA_COMPILER_CONTRACT_VERSION as OPENXIANGDA_V2_COMPILER_CONTRA
|
|
|
7
7
|
import { canonicalJson, sha256Digest as sha256Canonical } from '../canonical.js';
|
|
8
8
|
import { NativeDataFieldContractV2Error, nativeFieldRequiresCreateInputV2, parseNativeDataFieldsV2, parseNativeDataResourceInvariantsV2, validateNativeDataResourceReferencesV2, } from './data-field.js';
|
|
9
9
|
import { validateNativeDataResourceSurfaceV2 } from './data-surface.js';
|
|
10
|
+
import { nativeFieldSupportsSortV2 } from './field-query-plan.js';
|
|
10
11
|
import { nativeScopeSourceGrantPathSupportedV2, nativeScopeSourceSubjectPathSupportedV2, resolveNativeScopeSourceFieldPathV2, } from './scope-source-field-path.js';
|
|
11
12
|
import { NativeDataPolicyExpressionV2Error, nativeDataPolicyExpressionLeavesV2, nativeDataPolicyExpressionToCnfV2, } from './data-policy-expression.js';
|
|
12
13
|
const CONFIG_SCHEMA = 'openxiangda.config-bundle/v3';
|
|
@@ -23,7 +24,7 @@ const FIELD_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,62}$/;
|
|
|
23
24
|
const PROPERTY_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
24
25
|
const SECRET_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
25
26
|
const SECRET_ENV_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
26
|
-
const WORKFLOW_CODE_PATTERN = /^[
|
|
27
|
+
const WORKFLOW_CODE_PATTERN = /^[a-z][a-z0-9-]{2,63}$/;
|
|
27
28
|
const WORKFLOW_BINDING_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
|
|
28
29
|
const WORKFLOW_FACT_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.]{0,127}$/;
|
|
29
30
|
const WORKFLOW_SUMMARY_MAX_FIELDS = 16;
|
|
@@ -94,6 +95,7 @@ const EVENT_AUTHORIZATION_SYSTEM_FIELDS = new Set([
|
|
|
94
95
|
export const NATIVE_CONTRACT_CAPACITY_V2 = Object.freeze({
|
|
95
96
|
perspectives: 100,
|
|
96
97
|
resources: 100,
|
|
98
|
+
subjectReadSurfaces: 50,
|
|
97
99
|
capabilities: 2000,
|
|
98
100
|
operations: 500,
|
|
99
101
|
eventConsumers: 100,
|
|
@@ -196,6 +198,9 @@ export function compileNativeApplicationConfiguration(input) {
|
|
|
196
198
|
appCode,
|
|
197
199
|
resources: config.data.resources,
|
|
198
200
|
resourceContracts: expectedContract.resources,
|
|
201
|
+
...(expectedContract.subjectReadSurfaces
|
|
202
|
+
? { subjectReadSurfaces: expectedContract.subjectReadSurfaces }
|
|
203
|
+
: {}),
|
|
199
204
|
},
|
|
200
205
|
events: {
|
|
201
206
|
appCode,
|
|
@@ -309,6 +314,7 @@ export function compileRequiredPlatformCapabilitiesV3(config) {
|
|
|
309
314
|
path: operation.path,
|
|
310
315
|
capability: operation.capability,
|
|
311
316
|
platformAccess: operation.platformAccess || null,
|
|
317
|
+
...(operation.browser ? { browser: operation.browser } : {}),
|
|
312
318
|
ai: operation.ai || null,
|
|
313
319
|
})),
|
|
314
320
|
},
|
|
@@ -702,8 +708,14 @@ function validateConfigurationEnvelope(config, appCode) {
|
|
|
702
708
|
boundedArray(backend.operations, '/config/backend/operations', 500);
|
|
703
709
|
validateBackendSecrets(backend.secrets);
|
|
704
710
|
const data = object(config.data, '/config/data');
|
|
705
|
-
exactKeys(data, ['resources'], '/config/data', [
|
|
711
|
+
exactKeys(data, ['resources'], '/config/data', [
|
|
712
|
+
'resourceDetailRoutes',
|
|
713
|
+
'subjectReadSurfaces',
|
|
714
|
+
]);
|
|
706
715
|
boundedArray(data.resources, '/config/data/resources', 100);
|
|
716
|
+
if (data.subjectReadSurfaces !== undefined) {
|
|
717
|
+
boundedArray(data.subjectReadSurfaces, '/config/data/subjectReadSurfaces', NATIVE_CONTRACT_CAPACITY_V2.subjectReadSurfaces);
|
|
718
|
+
}
|
|
707
719
|
if (data.resourceDetailRoutes !== undefined) {
|
|
708
720
|
boundedArray(data.resourceDetailRoutes, '/config/data/resourceDetailRoutes', 100);
|
|
709
721
|
}
|
|
@@ -774,7 +786,12 @@ function validateContractEnvelope(contract, appCode, configDigest) {
|
|
|
774
786
|
'routeManifest',
|
|
775
787
|
'adminPages',
|
|
776
788
|
'adminNavigation',
|
|
777
|
-
], '/contracts', [
|
|
789
|
+
], '/contracts', [
|
|
790
|
+
'authentication',
|
|
791
|
+
'publicAccess',
|
|
792
|
+
'adminAccess',
|
|
793
|
+
'subjectReadSurfaces',
|
|
794
|
+
]);
|
|
778
795
|
equal(contract.schemaVersion, CONTRACT_SCHEMA, '/contracts/schemaVersion');
|
|
779
796
|
equal(contract.compilerContractVersion, COMPILER_CONTRACT_VERSION, '/contracts/compilerContractVersion');
|
|
780
797
|
requiredString(contract.generatorVersion, '/contracts/generatorVersion', 128);
|
|
@@ -782,6 +799,9 @@ function validateContractEnvelope(contract, appCode, configDigest) {
|
|
|
782
799
|
equal(contract.configDigest, configDigest, '/contracts/configDigest');
|
|
783
800
|
boundedArray(contract.perspectives, '/contracts/perspectives', NATIVE_CONTRACT_CAPACITY_V2.perspectives);
|
|
784
801
|
boundedArray(contract.resources, '/contracts/resources', NATIVE_CONTRACT_CAPACITY_V2.resources);
|
|
802
|
+
if (contract.subjectReadSurfaces !== undefined) {
|
|
803
|
+
boundedArray(contract.subjectReadSurfaces, '/contracts/subjectReadSurfaces', NATIVE_CONTRACT_CAPACITY_V2.subjectReadSurfaces);
|
|
804
|
+
}
|
|
785
805
|
boundedArray(contract.capabilities, '/contracts/capabilities', NATIVE_CONTRACT_CAPACITY_V2.capabilities);
|
|
786
806
|
boundedArray(contract.operations, '/contracts/operations', NATIVE_CONTRACT_CAPACITY_V2.operations);
|
|
787
807
|
boundedArray(contract.eventConsumers, '/contracts/eventConsumers', NATIVE_CONTRACT_CAPACITY_V2.eventConsumers);
|
|
@@ -904,6 +924,7 @@ function compileExpectedContract(config, configDigest, generatorVersion) {
|
|
|
904
924
|
]);
|
|
905
925
|
validateAdminNavigationReferences(config);
|
|
906
926
|
const adminPages = compileAdminPages(config);
|
|
927
|
+
const subjectReadSurfaces = compileSubjectReadSurfaces(config, capabilities);
|
|
907
928
|
return {
|
|
908
929
|
schemaVersion: CONTRACT_SCHEMA,
|
|
909
930
|
compilerContractVersion: COMPILER_CONTRACT_VERSION,
|
|
@@ -912,6 +933,7 @@ function compileExpectedContract(config, configDigest, generatorVersion) {
|
|
|
912
933
|
configDigest,
|
|
913
934
|
perspectives: config.perspectives,
|
|
914
935
|
resources: compileResources(config),
|
|
936
|
+
...(subjectReadSurfaces.length ? { subjectReadSurfaces } : {}),
|
|
915
937
|
capabilities: sorted(capabilities, item => item.code),
|
|
916
938
|
operations: compileOperations(config),
|
|
917
939
|
eventConsumers,
|
|
@@ -1118,6 +1140,145 @@ function compileResources(config) {
|
|
|
1118
1140
|
};
|
|
1119
1141
|
}), (item) => item.code);
|
|
1120
1142
|
}
|
|
1143
|
+
function compileSubjectReadSurfaces(config, capabilities) {
|
|
1144
|
+
if (config.data.subjectReadSurfaces === undefined)
|
|
1145
|
+
return [];
|
|
1146
|
+
const declarations = boundedArray(config.data.subjectReadSurfaces, '/config/data/subjectReadSurfaces', NATIVE_CONTRACT_CAPACITY_V2.subjectReadSurfaces);
|
|
1147
|
+
const capabilityCodes = new Set(capabilities.map(capability => String(capability.code)));
|
|
1148
|
+
const resources = new Map(config.data.resources.map((raw, index) => {
|
|
1149
|
+
const pointer = `/config/data/resources/${index}`;
|
|
1150
|
+
const resource = validateResource(raw, pointer, config.appCode);
|
|
1151
|
+
return [
|
|
1152
|
+
String(resource.code),
|
|
1153
|
+
{
|
|
1154
|
+
resource,
|
|
1155
|
+
fields: new Map(boundedArray(object(resource.schema, `${pointer}/schema`).fields, `${pointer}/schema/fields`, 500).map(field => {
|
|
1156
|
+
const declaration = object(field, `${pointer}/schema/fields`);
|
|
1157
|
+
return [String(declaration.code), declaration];
|
|
1158
|
+
})),
|
|
1159
|
+
},
|
|
1160
|
+
];
|
|
1161
|
+
}));
|
|
1162
|
+
const surfaceCodes = new Set();
|
|
1163
|
+
return sorted(declarations.map((raw, surfaceIndex) => {
|
|
1164
|
+
const pointer = `/config/data/subjectReadSurfaces/${surfaceIndex}`;
|
|
1165
|
+
const surface = object(raw, pointer);
|
|
1166
|
+
exactKeys(surface, ['code', 'name', 'capability', 'subject', 'relations'], pointer);
|
|
1167
|
+
const code = stableCode(surface.code, `${pointer}/code`);
|
|
1168
|
+
if (surfaceCodes.has(code)) {
|
|
1169
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_CODE_DUPLICATE', `${pointer}/code`);
|
|
1170
|
+
}
|
|
1171
|
+
surfaceCodes.add(code);
|
|
1172
|
+
const capability = capabilityCode(surface.capability, `${pointer}/capability`, config.appCode);
|
|
1173
|
+
if (!capabilityCodes.has(capability)) {
|
|
1174
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_CAPABILITY_NOT_DECLARED', `${pointer}/capability`);
|
|
1175
|
+
}
|
|
1176
|
+
const subject = object(surface.subject, `${pointer}/subject`);
|
|
1177
|
+
exactKeys(subject, ['resourceCode', 'fields'], `${pointer}/subject`);
|
|
1178
|
+
const subjectResourceCode = resourceCode(subject.resourceCode, `${pointer}/subject/resourceCode`);
|
|
1179
|
+
const subjectResource = resources.get(subjectResourceCode);
|
|
1180
|
+
if (!subjectResource) {
|
|
1181
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_RESOURCE_NOT_DECLARED', `${pointer}/subject/resourceCode`);
|
|
1182
|
+
}
|
|
1183
|
+
const subjectFields = uniqueStrings(subject.fields, `${pointer}/subject/fields`, 100).map((field, index) => fieldCodeValue(field, `${pointer}/subject/fields/${index}`));
|
|
1184
|
+
if (!subjectFields.length) {
|
|
1185
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_FIELDS_REQUIRED', `${pointer}/subject/fields`);
|
|
1186
|
+
}
|
|
1187
|
+
for (const [index, field] of subjectFields.entries()) {
|
|
1188
|
+
const declaration = subjectResource.fields.get(field);
|
|
1189
|
+
if (!declaration || declaration.type === 'subtable') {
|
|
1190
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_FIELD_INVALID', `${pointer}/subject/fields/${index}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const rawRelations = boundedArray(surface.relations, `${pointer}/relations`, 8);
|
|
1194
|
+
if (!rawRelations.length) {
|
|
1195
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_RELATIONS_REQUIRED', `${pointer}/relations`);
|
|
1196
|
+
}
|
|
1197
|
+
const relationCodes = new Set();
|
|
1198
|
+
let rowBudget = 0;
|
|
1199
|
+
const relations = sorted(rawRelations.map((rawRelation, relationIndex) => {
|
|
1200
|
+
const relationPointer = `${pointer}/relations/${relationIndex}`;
|
|
1201
|
+
const relation = object(rawRelation, relationPointer);
|
|
1202
|
+
exactKeys(relation, [
|
|
1203
|
+
'code',
|
|
1204
|
+
'resourceCode',
|
|
1205
|
+
'foreignKeyField',
|
|
1206
|
+
'fields',
|
|
1207
|
+
'limit',
|
|
1208
|
+
], relationPointer, ['order']);
|
|
1209
|
+
const relationCode = stableCode(relation.code, `${relationPointer}/code`);
|
|
1210
|
+
if (relationCodes.has(relationCode)) {
|
|
1211
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_RELATION_CODE_DUPLICATE', `${relationPointer}/code`);
|
|
1212
|
+
}
|
|
1213
|
+
relationCodes.add(relationCode);
|
|
1214
|
+
const relationResourceCode = resourceCode(relation.resourceCode, `${relationPointer}/resourceCode`);
|
|
1215
|
+
const relationResource = resources.get(relationResourceCode);
|
|
1216
|
+
if (!relationResource) {
|
|
1217
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_RESOURCE_NOT_DECLARED', `${relationPointer}/resourceCode`);
|
|
1218
|
+
}
|
|
1219
|
+
const foreignKeyField = fieldCodeValue(relation.foreignKeyField, `${relationPointer}/foreignKeyField`);
|
|
1220
|
+
const foreignKey = relationResource.fields.get(foreignKeyField);
|
|
1221
|
+
const foreignKeySource = object(foreignKey?.source || {}, `${relationPointer}/foreignKeyField/source`);
|
|
1222
|
+
if (!foreignKey ||
|
|
1223
|
+
!['uuid', 'resource-ref.single'].includes(String(foreignKey.type)) ||
|
|
1224
|
+
(foreignKey.type === 'resource-ref.single' &&
|
|
1225
|
+
(foreignKeySource.kind !== 'resource' ||
|
|
1226
|
+
foreignKeySource.resourceCode !== subjectResourceCode))) {
|
|
1227
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_RELATION_INVALID', `${relationPointer}/foreignKeyField`);
|
|
1228
|
+
}
|
|
1229
|
+
const fields = uniqueStrings(relation.fields, `${relationPointer}/fields`, 100).map((field, index) => fieldCodeValue(field, `${relationPointer}/fields/${index}`));
|
|
1230
|
+
if (!fields.length) {
|
|
1231
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_FIELDS_REQUIRED', `${relationPointer}/fields`);
|
|
1232
|
+
}
|
|
1233
|
+
for (const [index, field] of fields.entries()) {
|
|
1234
|
+
const declaration = relationResource.fields.get(field);
|
|
1235
|
+
if (!declaration || declaration.type === 'subtable') {
|
|
1236
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_FIELD_INVALID', `${relationPointer}/fields/${index}`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const order = boundedArray(relation.order || [], `${relationPointer}/order`, 10).map((rawOrder, orderIndex) => {
|
|
1240
|
+
const orderPointer = `${relationPointer}/order/${orderIndex}`;
|
|
1241
|
+
const item = object(rawOrder, orderPointer);
|
|
1242
|
+
exactKeys(item, ['field', 'direction'], orderPointer);
|
|
1243
|
+
const field = fieldCodeValue(item.field, `${orderPointer}/field`);
|
|
1244
|
+
const declaration = relationResource.fields.get(field);
|
|
1245
|
+
if ((!EVENT_AUTHORIZATION_SYSTEM_FIELDS.has(field) && !declaration) ||
|
|
1246
|
+
declaration?.type === 'subtable' ||
|
|
1247
|
+
(declaration &&
|
|
1248
|
+
!nativeFieldSupportsSortV2(declaration.type))) {
|
|
1249
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_ORDER_FIELD_INVALID', `${orderPointer}/field`);
|
|
1250
|
+
}
|
|
1251
|
+
if (!['asc', 'desc'].includes(String(item.direction))) {
|
|
1252
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_ORDER_DIRECTION_INVALID', `${orderPointer}/direction`);
|
|
1253
|
+
}
|
|
1254
|
+
return { field, direction: item.direction };
|
|
1255
|
+
});
|
|
1256
|
+
const limit = boundedInteger(relation.limit, `${relationPointer}/limit`, 1, 100);
|
|
1257
|
+
rowBudget += limit;
|
|
1258
|
+
return {
|
|
1259
|
+
code: relationCode,
|
|
1260
|
+
resourceCode: relationResourceCode,
|
|
1261
|
+
foreignKeyField,
|
|
1262
|
+
fields: uniqueSorted(fields),
|
|
1263
|
+
...(order.length ? { order } : {}),
|
|
1264
|
+
limit,
|
|
1265
|
+
};
|
|
1266
|
+
}), relation => relation.code);
|
|
1267
|
+
if (rowBudget > 400) {
|
|
1268
|
+
fail('NATIVE_SUBJECT_READ_SURFACE_ROW_LIMIT_EXCEEDED', `${pointer}/relations`);
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
code,
|
|
1272
|
+
name: requiredString(surface.name, `${pointer}/name`, 255),
|
|
1273
|
+
capability,
|
|
1274
|
+
subject: {
|
|
1275
|
+
resourceCode: subjectResourceCode,
|
|
1276
|
+
fields: uniqueSorted(subjectFields),
|
|
1277
|
+
},
|
|
1278
|
+
relations,
|
|
1279
|
+
};
|
|
1280
|
+
}), surface => surface.code);
|
|
1281
|
+
}
|
|
1121
1282
|
function compileResourceDetailRoutes(config) {
|
|
1122
1283
|
const declarations = config.data.resourceDetailRoutes === undefined
|
|
1123
1284
|
? []
|
|
@@ -1190,6 +1351,17 @@ function compileResourceDetailRoutes(config) {
|
|
|
1190
1351
|
return result;
|
|
1191
1352
|
}
|
|
1192
1353
|
function compileOperations(config) {
|
|
1354
|
+
const declaredResourceFields = new Map(config.data.resources.map((resource, resourceIndex) => {
|
|
1355
|
+
const pointer = `/config/data/resources/${resourceIndex}`;
|
|
1356
|
+
const declaration = validateResource(resource, pointer, config.appCode);
|
|
1357
|
+
return [
|
|
1358
|
+
declaration.code,
|
|
1359
|
+
new Map(declaration.schema.fields.map((field) => [
|
|
1360
|
+
String(field.code),
|
|
1361
|
+
field,
|
|
1362
|
+
])),
|
|
1363
|
+
];
|
|
1364
|
+
}));
|
|
1193
1365
|
const declaredResources = new Map(config.data.resources.map((resource, resourceIndex) => {
|
|
1194
1366
|
const pointer = `/config/data/resources/${resourceIndex}`;
|
|
1195
1367
|
const declaration = validateResource(resource, pointer, config.appCode);
|
|
@@ -1215,6 +1387,7 @@ function compileOperations(config) {
|
|
|
1215
1387
|
'responseSchema',
|
|
1216
1388
|
'description',
|
|
1217
1389
|
'platformAccess',
|
|
1390
|
+
'browser',
|
|
1218
1391
|
'ai',
|
|
1219
1392
|
], pointer, true);
|
|
1220
1393
|
const method = requiredString(operation.method, `${pointer}/method`, 16);
|
|
@@ -1224,6 +1397,7 @@ function compileOperations(config) {
|
|
|
1224
1397
|
const code = stableCode(operation.code, `${pointer}/code`);
|
|
1225
1398
|
validateBackendOperationAi(operation.ai, pointer, code, method, declaredResourceCodes);
|
|
1226
1399
|
const platformAccess = validateOperationPlatformAccess(operation.platformAccess, `${pointer}/platformAccess`, declaredResources, declaredWorkflowCodes, new Set(config.authz.roles.map((role) => String(role.code))));
|
|
1400
|
+
const browser = validateOperationBrowser(operation.browser, `${pointer}/browser`, method, operation.requestSchema, operation.responseSchema, declaredResources, declaredResourceFields, config.data.subjectReadSurfaces || []);
|
|
1227
1401
|
return {
|
|
1228
1402
|
code,
|
|
1229
1403
|
method,
|
|
@@ -1237,9 +1411,169 @@ function compileOperations(config) {
|
|
|
1237
1411
|
description: optionalString(operation.description, `${pointer}/description`, 2000),
|
|
1238
1412
|
}),
|
|
1239
1413
|
...(platformAccess ? { platformAccess } : {}),
|
|
1414
|
+
...(browser ? { browser } : {}),
|
|
1240
1415
|
};
|
|
1241
1416
|
}), (item) => item.code);
|
|
1242
1417
|
}
|
|
1418
|
+
function validateOperationBrowser(value, pointer, method, requestSchemaInput, responseSchemaInput, declaredResources, declaredResourceFields, subjectReadSurfacesInput) {
|
|
1419
|
+
if (value === undefined)
|
|
1420
|
+
return null;
|
|
1421
|
+
const browser = object(value, pointer);
|
|
1422
|
+
exactKeys(browser, ['exposure', 'behavior', 'idempotency', 'subject'], pointer, ['refreshTargets', 'fileIntent']);
|
|
1423
|
+
equal(browser.exposure, 'authenticated', `${pointer}/exposure`);
|
|
1424
|
+
const behavior = requiredString(browser.behavior, `${pointer}/behavior`, 32);
|
|
1425
|
+
if (!['read', 'controlled'].includes(behavior)) {
|
|
1426
|
+
fail('NATIVE_OPERATION_BROWSER_BEHAVIOR_INVALID', `${pointer}/behavior`);
|
|
1427
|
+
}
|
|
1428
|
+
const idempotency = requiredString(browser.idempotency, `${pointer}/idempotency`, 32);
|
|
1429
|
+
if (!['none', 'required'].includes(idempotency) ||
|
|
1430
|
+
(behavior === 'controlled' && idempotency !== 'required') ||
|
|
1431
|
+
(behavior === 'read' && idempotency !== 'none')) {
|
|
1432
|
+
fail('NATIVE_OPERATION_BROWSER_IDEMPOTENCY_INVALID', `${pointer}/idempotency`);
|
|
1433
|
+
}
|
|
1434
|
+
const subject = object(browser.subject, `${pointer}/subject`);
|
|
1435
|
+
exactKeys(subject, ['resourceCode', 'inputField'], `${pointer}/subject`);
|
|
1436
|
+
const resource = resourceCode(subject.resourceCode, `${pointer}/subject/resourceCode`);
|
|
1437
|
+
if (!declaredResources.has(resource)) {
|
|
1438
|
+
fail('NATIVE_OPERATION_BROWSER_SUBJECT_RESOURCE_NOT_DECLARED', `${pointer}/subject/resourceCode`);
|
|
1439
|
+
}
|
|
1440
|
+
const inputField = fieldCodeValue(subject.inputField, `${pointer}/subject/inputField`);
|
|
1441
|
+
const requestSchema = object(requestSchemaInput, `${pointer}/requestSchema`);
|
|
1442
|
+
const responseSchema = object(responseSchemaInput, `${pointer}/responseSchema`);
|
|
1443
|
+
if (requestSchema.type !== 'object' ||
|
|
1444
|
+
requestSchema.additionalProperties !== false ||
|
|
1445
|
+
responseSchema.type !== 'object' ||
|
|
1446
|
+
responseSchema.additionalProperties !== false) {
|
|
1447
|
+
fail('NATIVE_OPERATION_BROWSER_SCHEMA_ROOT_INVALID', `${pointer}/requestSchema`);
|
|
1448
|
+
}
|
|
1449
|
+
const requestProperties = object(requestSchema.properties, `${pointer}/requestSchema/properties`);
|
|
1450
|
+
const requiredFields = new Set(requestSchema.required === undefined
|
|
1451
|
+
? []
|
|
1452
|
+
: uniqueStrings(requestSchema.required, `${pointer}/requestSchema/required`, 200));
|
|
1453
|
+
if (!(inputField in requestProperties) || !requiredFields.has(inputField)) {
|
|
1454
|
+
fail('NATIVE_OPERATION_BROWSER_SUBJECT_INPUT_INVALID', `${pointer}/subject/inputField`);
|
|
1455
|
+
}
|
|
1456
|
+
const subjectInputSchema = object(requestProperties[inputField], `${pointer}/requestSchema/properties/${inputField}`);
|
|
1457
|
+
if (subjectInputSchema.type !== 'string' ||
|
|
1458
|
+
subjectInputSchema.format !== 'uuid') {
|
|
1459
|
+
fail('NATIVE_OPERATION_BROWSER_SUBJECT_INPUT_INVALID', `${pointer}/subject/inputField`);
|
|
1460
|
+
}
|
|
1461
|
+
const surfaces = new Map((Array.isArray(subjectReadSurfacesInput) ? subjectReadSurfacesInput : []).map((raw, index) => {
|
|
1462
|
+
const declaration = object(raw, `/config/data/subjectReadSurfaces/${index}`);
|
|
1463
|
+
return [
|
|
1464
|
+
String(declaration.code),
|
|
1465
|
+
String(object(declaration.subject, `/config/data/subjectReadSurfaces/${index}/subject`).resourceCode),
|
|
1466
|
+
];
|
|
1467
|
+
}));
|
|
1468
|
+
const refreshTargets = browser.refreshTargets === undefined
|
|
1469
|
+
? []
|
|
1470
|
+
: boundedArray(browser.refreshTargets, `${pointer}/refreshTargets`, 16);
|
|
1471
|
+
const refreshKeys = new Set();
|
|
1472
|
+
const normalizedTargets = sorted(refreshTargets.map((raw, index) => {
|
|
1473
|
+
const targetPointer = `${pointer}/refreshTargets/${index}`;
|
|
1474
|
+
const target = object(raw, targetPointer);
|
|
1475
|
+
exactKeys(target, ['kind', 'code'], targetPointer);
|
|
1476
|
+
equal(target.kind, 'subject-surface', `${targetPointer}/kind`);
|
|
1477
|
+
const code = stableCode(target.code, `${targetPointer}/code`);
|
|
1478
|
+
if (refreshKeys.has(code) ||
|
|
1479
|
+
!surfaces.has(code) ||
|
|
1480
|
+
surfaces.get(code) !== resource) {
|
|
1481
|
+
fail('NATIVE_OPERATION_BROWSER_REFRESH_TARGET_INVALID', `${targetPointer}/code`);
|
|
1482
|
+
}
|
|
1483
|
+
refreshKeys.add(code);
|
|
1484
|
+
return { kind: 'subject-surface', code };
|
|
1485
|
+
}), target => `${target.kind}:${target.code}`);
|
|
1486
|
+
const fileIntent = validateOperationFileIntent(browser.fileIntent, `${pointer}/fileIntent`, method, behavior, idempotency, resource, requestProperties, requiredFields, declaredResourceFields);
|
|
1487
|
+
return {
|
|
1488
|
+
exposure: 'authenticated',
|
|
1489
|
+
behavior,
|
|
1490
|
+
idempotency,
|
|
1491
|
+
subject: { resourceCode: resource, inputField },
|
|
1492
|
+
...(normalizedTargets.length ? { refreshTargets: normalizedTargets } : {}),
|
|
1493
|
+
...(fileIntent ? { fileIntent } : {}),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function validateOperationFileIntent(value, pointer, method, behavior, idempotency, subjectResourceCode, requestProperties, requiredFields, declaredResources) {
|
|
1497
|
+
if (value === undefined)
|
|
1498
|
+
return null;
|
|
1499
|
+
const intent = object(value, pointer);
|
|
1500
|
+
exactKeys(intent, [
|
|
1501
|
+
'recordResourceCode',
|
|
1502
|
+
'recordIdInputField',
|
|
1503
|
+
'relationField',
|
|
1504
|
+
'fileNameField',
|
|
1505
|
+
'contentTypeField',
|
|
1506
|
+
'sizeField',
|
|
1507
|
+
'purposes',
|
|
1508
|
+
'maxTtlSeconds',
|
|
1509
|
+
'maxBytes',
|
|
1510
|
+
], pointer);
|
|
1511
|
+
if (method !== 'GET' || behavior !== 'read' || idempotency !== 'none') {
|
|
1512
|
+
fail('NATIVE_OPERATION_FILE_INTENT_BEHAVIOR_INVALID', pointer);
|
|
1513
|
+
}
|
|
1514
|
+
const recordResourceCode = resourceCode(intent.recordResourceCode, `${pointer}/recordResourceCode`);
|
|
1515
|
+
const fields = declaredResources.get(recordResourceCode);
|
|
1516
|
+
if (!fields) {
|
|
1517
|
+
fail('NATIVE_OPERATION_FILE_INTENT_RESOURCE_NOT_DECLARED', `${pointer}/recordResourceCode`);
|
|
1518
|
+
}
|
|
1519
|
+
const recordIdInputField = fieldCodeValue(intent.recordIdInputField, `${pointer}/recordIdInputField`);
|
|
1520
|
+
const recordIdSchema = object(requestProperties[recordIdInputField], `${pointer}/recordIdInputField`);
|
|
1521
|
+
if (!requiredFields.has(recordIdInputField) ||
|
|
1522
|
+
recordIdSchema.type !== 'string' ||
|
|
1523
|
+
recordIdSchema.format !== 'uuid') {
|
|
1524
|
+
fail('NATIVE_OPERATION_FILE_INTENT_RECORD_INPUT_INVALID', `${pointer}/recordIdInputField`);
|
|
1525
|
+
}
|
|
1526
|
+
const relationField = fieldCodeValue(intent.relationField, `${pointer}/relationField`);
|
|
1527
|
+
const relation = fields.get(relationField);
|
|
1528
|
+
if (!relation ||
|
|
1529
|
+
!['uuid', 'resource-ref.single'].includes(String(relation.type)) ||
|
|
1530
|
+
(String(relation.type) === 'resource-ref.single' &&
|
|
1531
|
+
(String(object(relation.source, `${pointer}/relationField`).kind || '') !==
|
|
1532
|
+
'resource' ||
|
|
1533
|
+
String(object(relation.source, `${pointer}/relationField`).resourceCode || '') !==
|
|
1534
|
+
subjectResourceCode))) {
|
|
1535
|
+
fail('NATIVE_OPERATION_FILE_INTENT_RELATION_INVALID', `${pointer}/relationField`);
|
|
1536
|
+
}
|
|
1537
|
+
const metadata = [
|
|
1538
|
+
['fileNameField', ['text.short', 'text.long']],
|
|
1539
|
+
['contentTypeField', ['text.short', 'text.long']],
|
|
1540
|
+
['sizeField', ['number.integer']],
|
|
1541
|
+
];
|
|
1542
|
+
const normalizedMetadata = {};
|
|
1543
|
+
for (const [key, allowedTypes] of metadata) {
|
|
1544
|
+
const fieldCode = fieldCodeValue(intent[key], `${pointer}/${key}`);
|
|
1545
|
+
if (!allowedTypes.includes(String(fields.get(fieldCode)?.type))) {
|
|
1546
|
+
fail('NATIVE_OPERATION_FILE_INTENT_METADATA_INVALID', `${pointer}/${key}`);
|
|
1547
|
+
}
|
|
1548
|
+
normalizedMetadata[key] = fieldCode;
|
|
1549
|
+
}
|
|
1550
|
+
const purposes = sorted(uniqueStrings(intent.purposes, `${pointer}/purposes`, 2), item => item);
|
|
1551
|
+
if (!purposes.length ||
|
|
1552
|
+
purposes.some(purpose => !['preview', 'download'].includes(purpose))) {
|
|
1553
|
+
fail('NATIVE_OPERATION_FILE_INTENT_PURPOSE_INVALID', `${pointer}/purposes`);
|
|
1554
|
+
}
|
|
1555
|
+
const maxTtlSeconds = Number(intent.maxTtlSeconds);
|
|
1556
|
+
if (!Number.isSafeInteger(maxTtlSeconds) ||
|
|
1557
|
+
maxTtlSeconds < 1 ||
|
|
1558
|
+
maxTtlSeconds > 300) {
|
|
1559
|
+
fail('NATIVE_OPERATION_FILE_INTENT_TTL_INVALID', `${pointer}/maxTtlSeconds`);
|
|
1560
|
+
}
|
|
1561
|
+
const maxBytes = Number(intent.maxBytes);
|
|
1562
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 104857600) {
|
|
1563
|
+
fail('NATIVE_OPERATION_FILE_INTENT_SIZE_INVALID', `${pointer}/maxBytes`);
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
recordResourceCode,
|
|
1567
|
+
recordIdInputField,
|
|
1568
|
+
relationField,
|
|
1569
|
+
fileNameField: normalizedMetadata.fileNameField,
|
|
1570
|
+
contentTypeField: normalizedMetadata.contentTypeField,
|
|
1571
|
+
sizeField: normalizedMetadata.sizeField,
|
|
1572
|
+
purposes,
|
|
1573
|
+
maxTtlSeconds,
|
|
1574
|
+
maxBytes,
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1243
1577
|
function validateOperationPlatformAccess(value, pointer, declaredResources, declaredWorkflowCodes, declaredRoleCodes) {
|
|
1244
1578
|
if (value === undefined)
|
|
1245
1579
|
return null;
|
|
@@ -4030,6 +4364,7 @@ function validateWorkflowDefinition(definition, pointer) {
|
|
|
4030
4364
|
}
|
|
4031
4365
|
const targets = [];
|
|
4032
4366
|
if (node.kind === 'approval') {
|
|
4367
|
+
requiredString(node.title, `${nodePointer}/title`, 255);
|
|
4033
4368
|
workflowBindingCode(node.binding, `${nodePointer}/binding`);
|
|
4034
4369
|
if (!['single', 'any', 'all', 'sequence'].includes(node.mode)) {
|
|
4035
4370
|
fail('NATIVE_WORKFLOW_APPROVAL_MODE_INVALID', `${nodePointer}/mode`);
|
|
@@ -4059,6 +4394,7 @@ function validateWorkflowDefinition(definition, pointer) {
|
|
|
4059
4394
|
targets.push(requiredString(node.otherwise, `${nodePointer}/otherwise`, 128));
|
|
4060
4395
|
}
|
|
4061
4396
|
else {
|
|
4397
|
+
requiredString(node.title, `${nodePointer}/title`, 255);
|
|
4062
4398
|
requiredString(node.outcome, `${nodePointer}/outcome`, 128);
|
|
4063
4399
|
}
|
|
4064
4400
|
for (const target of targets) {
|