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.
@@ -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;AAwBrB,QAAA,MAAM,iBAAiB,mDAAmD,CAAC;AAC3E,QAAA,MAAM,yBAAyB,YAA2C,CAAC;AAgF3E,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;EAc7B,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,CA+InC;AAED;;;GAGG;AACH,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,UAAU,GACjB,uCAAuC,EAAE,CA2R3C;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"}
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 = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
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', ['resourceDetailRoutes']);
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', ['authentication', 'publicAccess', 'adminAccess']);
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) {