@object-ui/data-objectstack 17.5.0 → 17.6.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/dist/index.cjs +262 -357
  3. package/dist/index.d.cts +385 -448
  4. package/dist/index.d.ts +385 -448
  5. package/dist/index.js +258 -350
  6. package/package.json +5 -6
  7. package/dist/tsconfig.tsbuildinfo +0 -1
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/apiAccessDenied.test.ts +0 -233
  10. package/src/appAccessProbe.test.ts +0 -147
  11. package/src/bulkDelete.test.ts +0 -75
  12. package/src/bulkUpdate.test.ts +0 -93
  13. package/src/cache/MetadataCache.test.ts +0 -477
  14. package/src/cache/MetadataCache.ts +0 -289
  15. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  16. package/src/connection.test.ts +0 -188
  17. package/src/contracts.ts +0 -115
  18. package/src/deleteView.homes.test.ts +0 -389
  19. package/src/deleteViewContract.types.test.ts +0 -246
  20. package/src/errors.test.ts +0 -426
  21. package/src/errors.ts +0 -293
  22. package/src/exportDownload.test.ts +0 -141
  23. package/src/filter-entry-translation.test.ts +0 -360
  24. package/src/filter-operator-ast-parity.test.ts +0 -135
  25. package/src/getObjectSchema.test.ts +0 -145
  26. package/src/httpFailureLogging.test.ts +0 -109
  27. package/src/importJob.test.ts +0 -71
  28. package/src/index.ts +0 -4442
  29. package/src/integration.ts +0 -200
  30. package/src/listViewOverrides.test.ts +0 -241
  31. package/src/listViews.test.ts +0 -175
  32. package/src/metadata-client.get-envelope.test.ts +0 -171
  33. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  34. package/src/metadata-client.test.ts +0 -303
  35. package/src/metadata-client.ts +0 -1046
  36. package/src/occ.test.ts +0 -101
  37. package/src/onMutation.test.ts +0 -589
  38. package/src/onSaveAdvisory.test.ts +0 -311
  39. package/src/onWriteWarning.test.ts +0 -273
  40. package/src/orderby-serialization.test.ts +0 -115
  41. package/src/queryDataset.test.ts +0 -543
  42. package/src/searchAll.test.ts +0 -120
  43. package/src/security.ts +0 -243
  44. package/src/spec-symbol-batch6.test.ts +0 -326
  45. package/src/studio.ts +0 -152
  46. package/src/updateView.draft.test.ts +0 -242
  47. package/src/userState.test.ts +0 -315
  48. package/src/userState.ts +0 -238
  49. package/src/v3-compat.test.ts +0 -229
  50. package/src/validation-error.test.ts +0 -94
  51. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  52. package/src/viewInvalidationSeam.pin.test.ts +0 -162
package/dist/index.cjs CHANGED
@@ -24,31 +24,29 @@ var index_exports = {};
24
24
  __export(index_exports, {
25
25
  API_ACCESS_DENIED_CODES: () => API_ACCESS_DENIED_CODES,
26
26
  APP_PERMISSION_DENIED_CODE: () => APP_PERMISSION_DENIED_CODE,
27
+ AnalyticsDatasetNotFoundError: () => AnalyticsDatasetNotFoundError,
27
28
  AnalyticsNotInstalledError: () => AnalyticsNotInstalledError,
28
29
  AnalyticsQueryRejectedError: () => AnalyticsQueryRejectedError,
30
+ AnalyticsUnauthenticatedError: () => AnalyticsUnauthenticatedError,
29
31
  AuthenticationError: () => AuthenticationError,
30
32
  BulkOperationError: () => BulkOperationError,
31
33
  ConcurrentUpdateError: () => ConcurrentUpdateError,
32
34
  ConnectionError: () => ConnectionError,
33
35
  DataApiValidationError: () => DataApiValidationError,
34
36
  FILTER_OPERATOR_ALIASES: () => FILTER_OPERATOR_ALIASES,
35
- IntegrationManager: () => IntegrationManager,
36
37
  MalformedFilterError: () => MalformedFilterError,
37
38
  MetadataClient: () => MetadataClient,
38
39
  MetadataNotFoundError: () => MetadataNotFoundError,
39
40
  ObjectStackAdapter: () => ObjectStackAdapter,
40
41
  ObjectStackError: () => ObjectStackError,
41
- SecurityManager: () => SecurityManager,
42
- calculateAutoLayout: () => calculateAutoLayout,
42
+ VIEW_OVERLAY_OWNED_KEYS: () => VIEW_OVERLAY_OWNED_KEYS,
43
43
  classifyAnalyticsFailure: () => classifyAnalyticsFailure,
44
44
  clearSharedDiscoveryCache: () => clearSharedDiscoveryCache,
45
- createDefaultCanvasConfig: () => createDefaultCanvasConfig,
46
45
  createErrorFromResponse: () => createErrorFromResponse,
47
46
  createObjectStackAdapter: () => createObjectStackAdapter,
48
47
  createObjectStackUserStateAdapter: () => createObjectStackUserStateAdapter,
49
48
  createQuietHttpLogger: () => createQuietHttpLogger,
50
49
  formatHttpFailureMessage: () => formatHttpFailureMessage,
51
- generateContractManifest: () => generateContractManifest,
52
50
  getSharedDiscovery: () => getSharedDiscovery,
53
51
  is404Error: () => is404Error,
54
52
  isAnalyticsNotInstalledError: () => isAnalyticsNotInstalledError,
@@ -58,12 +56,11 @@ __export(index_exports, {
58
56
  isErrorType: () => isErrorType,
59
57
  isMalformedFilterError: () => isMalformedFilterError,
60
58
  isObjectStackError: () => isObjectStackError,
59
+ narrowPersonalizationOverlay: () => narrowPersonalizationOverlay,
61
60
  normaliseClientError: () => normaliseClientError,
62
61
  readSaveAdvisories: () => readSaveAdvisories,
63
62
  readTransactionalBatchCapability: () => readTransactionalBatchCapability,
64
63
  serializeOrderBy: () => serializeOrderBy,
65
- snapToGrid: () => snapToGrid,
66
- validatePluginContract: () => validatePluginContract,
67
64
  viewItemObjectName: () => viewItemObjectName
68
65
  });
69
66
  module.exports = __toCommonJS(index_exports);
@@ -310,7 +307,6 @@ var MetadataClient = class _MetadataClient {
310
307
  "Content-Type": "application/json"
311
308
  };
312
309
  if (options.ifMatch) headers["If-Match"] = options.ifMatch;
313
- if (options.actor) headers["X-Actor"] = options.actor;
314
310
  const res = await this.fetchImpl(url, {
315
311
  method: "PUT",
316
312
  headers,
@@ -359,13 +355,39 @@ var MetadataClient = class _MetadataClient {
359
355
  return inner;
360
356
  }
361
357
  /**
362
- * Get the 3-state layered view of a metadata item (Phase 3a). Returns
363
- * `code` (the artifact / fallback default), `overlay` (the saved
364
- * customisation, if any), and `effective` (what the runtime sees).
358
+ * Get the 3-state layered view of a metadata item: `code` (the packaged
359
+ * artifact baseline), `overlay` (the tenant customisation row alone) and
360
+ * `effective` (the merged value the runtime sees).
361
+ *
362
+ * Reads **`GET /meta/:type/:name/layers`** — the path the framework declares
363
+ * for this projection, with a response schema of its own
364
+ * (`GetMetaItemLayeredResponseSchema`, objectstack#5882 ruling B). It used to
365
+ * be reached by hanging a `layers` flag on the ordinary item read, which made
366
+ * one route answer two unrelated representations while `packages/spec`
367
+ * declared only one of them. That spelling still answers this same body
368
+ * inside its deprecation window (the response carries RFC 9745
369
+ * `Deprecation: true` and an RFC 8288 `Link: rel="successor-version"` back to
370
+ * this path) and is scheduled for removal upstream, so nothing here may
371
+ * depend on it — the repo-wide ratchet is
372
+ * `scripts/__tests__/layered-read-declared-path-4016.test.ts`.
373
+ *
374
+ * The request is built here rather than delegated to `@objectstack/client`
375
+ * because the SDK expresses no layered read in EITHER spelling: the
376
+ * framework's REST route ledger records this route as `server-only`,
377
+ * "consumed by objectui over plain HTTP", and whether the SDK should express
378
+ * it is an open upstream product call.
379
+ *
380
+ * One behaviour delta rides along with the path, and it is the server's
381
+ * choice rather than ours: the retired flag FELL THROUGH to the plain item
382
+ * read on a backend whose protocol implementation had no layered support,
383
+ * answering the `{ type, name, item }` envelope. A dedicated path refuses to
384
+ * answer a different resource under this one's declared shape, so it returns
385
+ * 501 `NOT_IMPLEMENTED` instead — which surfaces here as a thrown error
386
+ * rather than a view with `code` and `overlay` silently blank.
365
387
  */
366
388
  async layered(type, name, options = {}) {
367
- const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
368
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
389
+ const qs = options.packageId ? `?package=${encodeURIComponent(options.packageId)}` : "";
390
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/layers${qs}`;
369
391
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
370
392
  if (res.status === 404) {
371
393
  return { code: null, overlay: null, overlayScope: null, effective: null };
@@ -455,7 +477,6 @@ var MetadataClient = class _MetadataClient {
455
477
  const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
456
478
  const headers = { ...this.headers };
457
479
  if (options.ifMatch) headers["If-Match"] = options.ifMatch;
458
- if (options.actor) headers["X-Actor"] = options.actor;
459
480
  const res = await this.fetchImpl(url, { method: "DELETE", headers });
460
481
  if (!res.ok) throw await parseError(res);
461
482
  return await res.json();
@@ -473,7 +494,6 @@ var MetadataClient = class _MetadataClient {
473
494
  ...this.headers,
474
495
  "Content-Type": "application/json"
475
496
  };
476
- if (options.actor) headers["X-Actor"] = options.actor;
477
497
  const res = await this.fetchImpl(url, {
478
498
  method: "POST",
479
499
  headers,
@@ -496,7 +516,6 @@ var MetadataClient = class _MetadataClient {
496
516
  ...this.headers,
497
517
  "Content-Type": "application/json"
498
518
  };
499
- if (options.actor) headers["X-Actor"] = options.actor;
500
519
  const res = await this.fetchImpl(url, {
501
520
  method: "POST",
502
521
  headers,
@@ -1097,52 +1116,6 @@ function isErrorType(error, errorClass) {
1097
1116
  return error instanceof errorClass;
1098
1117
  }
1099
1118
 
1100
- // src/contracts.ts
1101
- function validatePluginContract(contract) {
1102
- const errors = [];
1103
- const warnings = [];
1104
- if (!contract.name || contract.name.trim().length === 0) {
1105
- errors.push({ field: "name", message: "Plugin name is required", code: "MISSING_NAME" });
1106
- }
1107
- if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
1108
- errors.push({ field: "version", message: "Valid semver version is required", code: "INVALID_VERSION" });
1109
- }
1110
- if (!contract.exports || contract.exports.length === 0) {
1111
- errors.push({ field: "exports", message: "At least one export is required", code: "NO_EXPORTS" });
1112
- }
1113
- if (contract.exports) {
1114
- const validTypes = ["component", "hook", "utility", "provider"];
1115
- for (const exp of contract.exports) {
1116
- if (!exp.name) {
1117
- errors.push({ field: "exports.name", message: "Export name is required", code: "MISSING_EXPORT_NAME" });
1118
- }
1119
- if (!validTypes.includes(exp.type)) {
1120
- errors.push({ field: "exports.type", message: `Invalid export type: ${exp.type}`, code: "INVALID_EXPORT_TYPE" });
1121
- }
1122
- }
1123
- }
1124
- if (!contract.permissions || contract.permissions.length === 0) {
1125
- warnings.push("No permissions declared \u2014 plugin will have minimal access");
1126
- }
1127
- return { valid: errors.length === 0, errors, warnings };
1128
- }
1129
- function generateContractManifest(contract) {
1130
- return {
1131
- $schema: "https://objectui.org/schemas/plugin-contract-v1.json",
1132
- name: contract.name,
1133
- version: contract.version,
1134
- peerDependencies: contract.peerDependencies ?? {},
1135
- exports: contract.exports.map((exp) => ({
1136
- name: exp.name,
1137
- type: exp.type,
1138
- description: exp.description ?? ""
1139
- })),
1140
- permissions: contract.permissions ?? [],
1141
- api: contract.api ?? {},
1142
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1143
- };
1144
- }
1145
-
1146
1119
  // src/userState.ts
1147
1120
  var DEFAULT_RESOURCE = "sys_user_preference";
1148
1121
  function createObjectStackUserStateAdapter(options) {
@@ -1245,267 +1218,6 @@ function createObjectStackUserStateAdapter(options) {
1245
1218
  };
1246
1219
  }
1247
1220
 
1248
- // src/integration.ts
1249
- var IntegrationManager = class {
1250
- constructor() {
1251
- __publicField(this, "integrations", /* @__PURE__ */ new Map());
1252
- }
1253
- /**
1254
- * Register a new integration.
1255
- */
1256
- register(id, config) {
1257
- this.integrations.set(id, config);
1258
- }
1259
- /**
1260
- * Remove an integration.
1261
- */
1262
- unregister(id) {
1263
- this.integrations.delete(id);
1264
- }
1265
- /**
1266
- * Get all registered integrations.
1267
- */
1268
- getAll() {
1269
- return new Map(this.integrations);
1270
- }
1271
- /**
1272
- * Get integrations that match a specific event.
1273
- */
1274
- getForEvent(event) {
1275
- return Array.from(this.integrations.values()).filter(
1276
- (integration) => integration.enabled && integration.triggers?.some((t) => t.event === event)
1277
- );
1278
- }
1279
- /**
1280
- * Dispatch an event to all matching integrations.
1281
- * Returns results for each integration.
1282
- */
1283
- async dispatch(event, payload) {
1284
- const matching = this.getForEvent(event);
1285
- const results = [];
1286
- for (const [id, integration] of this.integrations) {
1287
- if (!matching.includes(integration)) continue;
1288
- try {
1289
- await this.send(integration, payload);
1290
- results.push({ id, success: true });
1291
- } catch (err) {
1292
- results.push({ id, success: false, error: err.message });
1293
- }
1294
- }
1295
- return results;
1296
- }
1297
- /**
1298
- * Send payload to a specific integration.
1299
- */
1300
- async send(integration, payload) {
1301
- switch (integration.provider) {
1302
- case "webhook": {
1303
- const cfg = integration.config;
1304
- const url = cfg.url;
1305
- try {
1306
- const parsed = new URL(url);
1307
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1308
- throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
1309
- }
1310
- } catch (e) {
1311
- if (e instanceof TypeError) {
1312
- throw new Error(`Invalid webhook URL: ${url}`);
1313
- }
1314
- throw e;
1315
- }
1316
- await fetch(url, {
1317
- method: cfg.method,
1318
- headers: {
1319
- "Content-Type": "application/json",
1320
- ...cfg.headers
1321
- },
1322
- body: JSON.stringify(payload)
1323
- });
1324
- break;
1325
- }
1326
- case "slack": {
1327
- const cfg = integration.config;
1328
- const url = cfg.webhookUrl;
1329
- try {
1330
- const parsed = new URL(url);
1331
- if (parsed.protocol !== "https:") {
1332
- throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
1333
- }
1334
- } catch (e) {
1335
- if (e instanceof TypeError) {
1336
- throw new Error(`Invalid Slack webhook URL: ${url}`);
1337
- }
1338
- throw e;
1339
- }
1340
- await fetch(url, {
1341
- method: "POST",
1342
- headers: { "Content-Type": "application/json" },
1343
- body: JSON.stringify({
1344
- channel: cfg.channel,
1345
- username: cfg.username,
1346
- icon_emoji: cfg.iconEmoji,
1347
- text: JSON.stringify(payload)
1348
- })
1349
- });
1350
- break;
1351
- }
1352
- // Email and other providers would require server-side implementation
1353
- default:
1354
- break;
1355
- }
1356
- }
1357
- };
1358
-
1359
- // src/security.ts
1360
- var SecurityManager = class {
1361
- constructor(policy = {}) {
1362
- __publicField(this, "policy");
1363
- __publicField(this, "auditLog", []);
1364
- this.policy = policy;
1365
- }
1366
- /**
1367
- * Generate a CSP header string from the configuration.
1368
- */
1369
- generateCSPHeader() {
1370
- const csp = this.policy.csp;
1371
- if (!csp) return "";
1372
- const directives = [];
1373
- if (csp.scriptSrc?.length) directives.push(`script-src ${csp.scriptSrc.join(" ")}`);
1374
- if (csp.styleSrc?.length) directives.push(`style-src ${csp.styleSrc.join(" ")}`);
1375
- if (csp.imgSrc?.length) directives.push(`img-src ${csp.imgSrc.join(" ")}`);
1376
- if (csp.connectSrc?.length) directives.push(`connect-src ${csp.connectSrc.join(" ")}`);
1377
- if (csp.fontSrc?.length) directives.push(`font-src ${csp.fontSrc.join(" ")}`);
1378
- if (csp.frameSrc?.length) directives.push(`frame-src ${csp.frameSrc.join(" ")}`);
1379
- if (csp.reportUri) directives.push(`report-uri ${csp.reportUri}`);
1380
- return directives.join("; ");
1381
- }
1382
- /**
1383
- * Record an audit log entry.
1384
- */
1385
- recordAudit(entry) {
1386
- if (!this.policy.auditLog?.enabled) return;
1387
- if (this.policy.auditLog.events && !this.policy.auditLog.events.includes(entry.event)) return;
1388
- const fullEntry = {
1389
- ...entry,
1390
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1391
- };
1392
- this.auditLog.push(fullEntry);
1393
- const dest = this.policy.auditLog.destination ?? "console";
1394
- if (dest === "console" || dest === "both") {
1395
- console.info("[AUDIT]", fullEntry.event, fullEntry.userId, fullEntry.resource ?? "");
1396
- }
1397
- }
1398
- /**
1399
- * Get audit log entries.
1400
- */
1401
- getAuditLog(filter) {
1402
- let entries = [...this.auditLog];
1403
- if (filter?.event) entries = entries.filter((e) => e.event === filter.event);
1404
- if (filter?.userId) entries = entries.filter((e) => e.userId === filter.userId);
1405
- if (filter?.since) entries = entries.filter((e) => e.timestamp >= filter.since);
1406
- return entries;
1407
- }
1408
- /**
1409
- * Apply data masking to a record.
1410
- */
1411
- maskRecord(record, userRoles = []) {
1412
- if (!this.policy.dataMasking?.rules?.length) return record;
1413
- const masked = { ...record };
1414
- const maskChar = this.policy.dataMasking.maskChar ?? "*";
1415
- for (const rule of this.policy.dataMasking.rules) {
1416
- if (!(rule.field in masked) || masked[rule.field] == null) continue;
1417
- if (rule.exemptRoles?.some((role) => userRoles.includes(role))) continue;
1418
- const value = String(masked[rule.field]);
1419
- switch (rule.strategy) {
1420
- case "full":
1421
- masked[rule.field] = maskChar.repeat(value.length);
1422
- break;
1423
- case "partial": {
1424
- const visible = rule.visibleChars ?? 4;
1425
- if (value.length <= visible) {
1426
- masked[rule.field] = maskChar.repeat(value.length);
1427
- } else {
1428
- masked[rule.field] = value.slice(0, visible) + maskChar.repeat(value.length - visible);
1429
- }
1430
- break;
1431
- }
1432
- case "hash":
1433
- masked[rule.field] = `[HASHED:${simpleHash(value)}]`;
1434
- break;
1435
- case "redact":
1436
- masked[rule.field] = "[REDACTED]";
1437
- break;
1438
- }
1439
- }
1440
- return masked;
1441
- }
1442
- /**
1443
- * Update the security policy.
1444
- */
1445
- updatePolicy(policy) {
1446
- this.policy = { ...this.policy, ...policy };
1447
- }
1448
- /**
1449
- * Get current security policy.
1450
- */
1451
- getPolicy() {
1452
- return { ...this.policy };
1453
- }
1454
- };
1455
- function simpleHash(str) {
1456
- let hash = 0;
1457
- for (let i = 0; i < str.length; i++) {
1458
- const char = str.charCodeAt(i);
1459
- hash = (hash << 5) - hash + char;
1460
- hash = hash & hash;
1461
- }
1462
- return Math.abs(hash).toString(36);
1463
- }
1464
-
1465
- // src/studio.ts
1466
- function createDefaultCanvasConfig(overrides) {
1467
- return {
1468
- width: 1200,
1469
- height: 800,
1470
- background: "grid",
1471
- gridSize: 8,
1472
- snapToGrid: true,
1473
- zoom: {
1474
- min: 0.25,
1475
- max: 3,
1476
- step: 0.1,
1477
- current: 1
1478
- },
1479
- panOffset: { x: 0, y: 0 },
1480
- showMinimap: false,
1481
- minimapPosition: "bottom-right",
1482
- ...overrides
1483
- };
1484
- }
1485
- function snapToGrid(x, y, gridSize) {
1486
- return {
1487
- x: Math.round(x / gridSize) * gridSize,
1488
- y: Math.round(y / gridSize) * gridSize
1489
- };
1490
- }
1491
- function calculateAutoLayout(items, canvasWidth, padding = 40, gap = 40) {
1492
- const positions = [];
1493
- let currentX = padding;
1494
- let currentY = padding;
1495
- let rowMaxHeight = 0;
1496
- for (const item of items) {
1497
- if (currentX + item.width + padding > canvasWidth) {
1498
- currentX = padding;
1499
- currentY += rowMaxHeight + gap;
1500
- rowMaxHeight = 0;
1501
- }
1502
- positions.push({ id: item.id, x: currentX, y: currentY });
1503
- currentX += item.width + gap;
1504
- rowMaxHeight = Math.max(rowMaxHeight, item.height);
1505
- }
1506
- return positions;
1507
- }
1508
-
1509
1221
  // src/index.ts
1510
1222
  var FILTER_OPERATOR_ALIASES = {
1511
1223
  equals: "=",
@@ -1533,6 +1245,15 @@ var FILTER_OPERATOR_ALIASES = {
1533
1245
  notin: "nin",
1534
1246
  nin: "nin",
1535
1247
  contains: "contains",
1248
+ // Case-insensitive contains. A canonical `VIEW_FILTER_OPERATORS` member that
1249
+ // arrived with `@objectstack/spec` 17.1.0 (objectui#5328), and an IDENTITY row
1250
+ // like `contains` above: `icontains` is itself a member of
1251
+ // `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the
1252
+ // AST takes — nothing is translated and no case-sensitivity is lost.
1253
+ // Declared rather than left to the `?? op` fall-through on the rule this
1254
+ // table's test states: the AST gate accepting a spelling is not the driver
1255
+ // compiling it into a WHERE clause.
1256
+ icontains: "icontains",
1536
1257
  not_contains: "notcontains",
1537
1258
  notcontains: "notcontains",
1538
1259
  starts_with: "startswith",
@@ -1693,21 +1414,83 @@ function isAppPermissionDeniedError(error) {
1693
1414
  return errorCodeIs(error, APP_PERMISSION_DENIED_CODE);
1694
1415
  }
1695
1416
  var AnalyticsNotInstalledError = class extends Error {
1696
- constructor(surface, detail) {
1417
+ constructor(surface, detail, serverCode) {
1697
1418
  super(
1698
1419
  `Analytics capability is not installed on this deployment \u2014 ${surface} is unavailable. Install @objectstack/service-analytics and mount AnalyticsServicePlugin to enable it.` + (detail ? ` (server said: ${detail})` : "")
1699
1420
  );
1700
1421
  __publicField(this, "code", "ANALYTICS_NOT_INSTALLED");
1701
1422
  /** The surface that was unavailable, for the message a host renders. */
1702
1423
  __publicField(this, "surface");
1424
+ /**
1425
+ * The server's own ADR-0112 code — the field this branch was chosen BY, when
1426
+ * a producer named one (`NOT_IMPLEMENTED` from the mounted route with no
1427
+ * service behind it, `ROUTE_NOT_FOUND` from the dispatcher when the route is
1428
+ * not mounted at all). Absent for the no-code residual, where a bare
1429
+ * transport 404/501 is all there was to read.
1430
+ *
1431
+ * Additive (objectui#5663): carried so a reader can audit that the headline
1432
+ * and the quoted `detail` below came off the same answer.
1433
+ */
1434
+ __publicField(this, "serverCode");
1703
1435
  this.name = "AnalyticsNotInstalledError";
1704
1436
  this.surface = surface;
1437
+ this.serverCode = serverCode;
1705
1438
  }
1706
1439
  };
1707
1440
  function isAnalyticsNotInstalledError(error) {
1708
1441
  if (!error || typeof error !== "object") return false;
1709
1442
  return error.code === "ANALYTICS_NOT_INSTALLED";
1710
1443
  }
1444
+ var AnalyticsDatasetNotFoundError = class extends Error {
1445
+ constructor(opts) {
1446
+ const subject = opts.datasetName ? `Dataset "${opts.datasetName}"` : "The requested dataset";
1447
+ super(
1448
+ `${subject} is not defined in this environment \u2014 the app that defines it is missing or out of date. Analytics itself is installed and answering, so the fix is to upgrade the installed app, not to install a server plugin.` + (opts.serverMessage ? ` (server said: ${opts.serverMessage})` : "")
1449
+ );
1450
+ __publicField(this, "code", "ANALYTICS_DATASET_NOT_FOUND");
1451
+ /**
1452
+ * The dataset this query asked for, taken from the REQUEST rather than parsed
1453
+ * back out of the server's prose: the request is what we know for certain,
1454
+ * and re-reading the name out of a message would make the headline depend on
1455
+ * that message's WORDING — the coupling framework#5367 spent a whole issue
1456
+ * removing from the producing route.
1457
+ */
1458
+ __publicField(this, "datasetName");
1459
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1460
+ __publicField(this, "serverCode");
1461
+ /** The server's own message, quoted verbatim in the parenthetical. */
1462
+ __publicField(this, "serverMessage");
1463
+ this.name = "AnalyticsDatasetNotFoundError";
1464
+ this.datasetName = opts.datasetName;
1465
+ this.serverCode = opts.serverCode;
1466
+ this.serverMessage = opts.serverMessage;
1467
+ }
1468
+ };
1469
+ var AnalyticsUnauthenticatedError = class extends Error {
1470
+ constructor(opts = {}) {
1471
+ super(
1472
+ "Analytics query refused: this session is not authenticated. Sign in again and retry \u2014 the request was refused before it ran, so it says nothing about whether the analytics capability is installed." + (opts.serverMessage ? ` (server said: ${opts.serverMessage})` : "")
1473
+ );
1474
+ __publicField(this, "code", "ANALYTICS_UNAUTHENTICATED");
1475
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1476
+ __publicField(this, "serverCode");
1477
+ /** The server's own message, quoted verbatim in the parenthetical. */
1478
+ __publicField(this, "serverMessage");
1479
+ this.name = "AnalyticsUnauthenticatedError";
1480
+ this.serverCode = opts.serverCode;
1481
+ this.serverMessage = opts.serverMessage;
1482
+ }
1483
+ };
1484
+ function readAnalyticsErrorEnvelope(body) {
1485
+ if (!body || typeof body !== "object") return {};
1486
+ const flat = body;
1487
+ const wrapped = flat.error && typeof flat.error === "object" ? flat.error : void 0;
1488
+ const source = wrapped ?? flat;
1489
+ const code = typeof source.code === "string" && source.code.length > 0 ? source.code : void 0;
1490
+ const message = typeof source.message === "string" && source.message.length > 0 ? source.message : typeof flat.error === "string" && flat.error.length > 0 ? flat.error : void 0;
1491
+ return { code, message };
1492
+ }
1493
+ var ANALYTICS_ABSENT_STATUSES = [404, 501];
1711
1494
  var AnalyticsQueryRejectedError = class extends Error {
1712
1495
  constructor(detail, serverCode) {
1713
1496
  super(
@@ -1722,13 +1505,22 @@ var AnalyticsQueryRejectedError = class extends Error {
1722
1505
  };
1723
1506
  function classifyAnalyticsFailure(error) {
1724
1507
  const err = error ?? {};
1725
- const code = typeof err.code === "string" ? err.code : void 0;
1508
+ const code = typeof err.code === "string" && err.code.length > 0 ? err.code : void 0;
1726
1509
  const message = typeof err.message === "string" ? err.message : void 0;
1727
1510
  const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
1728
- if (code === "VALIDATION_FAILED" || status === 400) return { kind: "rejected", code, message };
1729
- if (status === 404 || status === 501 || code === "NOT_IMPLEMENTED" || code === "ROUTE_NOT_FOUND") {
1511
+ if (errorCodeIsAnyOf({ code }, ["NOT_IMPLEMENTED", "ROUTE_NOT_FOUND"])) {
1730
1512
  return { kind: "not-installed", code, message };
1731
1513
  }
1514
+ if (errorCodeIs({ code }, "VALIDATION_FAILED")) return { kind: "rejected", code, message };
1515
+ if (errorCodeIs({ code }, "UNAUTHENTICATED")) return { kind: "unauthenticated", code, message };
1516
+ if (errorCodeIs({ code }, "CUBE_NOT_FOUND")) return { kind: "cube-not-found", code, message };
1517
+ if (code === void 0) {
1518
+ if (status !== void 0 && ANALYTICS_ABSENT_STATUSES.includes(status)) {
1519
+ return { kind: "not-installed", code, message };
1520
+ }
1521
+ if (status === 400) return { kind: "rejected", code, message };
1522
+ if (status === 401) return { kind: "unauthenticated", code, message };
1523
+ }
1732
1524
  return { kind: "unknown", code, message };
1733
1525
  }
1734
1526
  var ConcurrentUpdateError = class extends Error {
@@ -1801,8 +1593,14 @@ function createQuietHttpLogger() {
1801
1593
  return false;
1802
1594
  };
1803
1595
  const logger = {
1804
- debug: (message, meta) => console.debug(message, meta ?? ""),
1805
- info: (message, meta) => console.info(message, meta ?? ""),
1596
+ debug: (message, meta) => (
1597
+ // eslint-disable-next-line no-console -- objectui#4029, see comment above
1598
+ console.debug(message, meta ?? "")
1599
+ ),
1600
+ info: (message, meta) => (
1601
+ // eslint-disable-next-line no-console -- objectui#4029, see comment above
1602
+ console.info(message, meta ?? "")
1603
+ ),
1806
1604
  warn: (message, meta) => console.warn(message, meta ?? ""),
1807
1605
  error: (message, error, meta) => {
1808
1606
  if (isExpected404(meta)) {
@@ -1815,6 +1613,7 @@ function createQuietHttpLogger() {
1815
1613
  console.error(formatHttpFailureMessage(message, meta) ?? message, error ?? "", meta ?? "");
1816
1614
  },
1817
1615
  fatal: (message, error, meta) => console.error(formatHttpFailureMessage(message, meta) ?? message, error ?? "", meta ?? ""),
1616
+ // eslint-disable-next-line no-console -- objectui#4029, see comment above
1818
1617
  log: (message, ...args) => console.log(message, ...args),
1819
1618
  child: () => logger,
1820
1619
  withTrace: () => logger
@@ -1857,6 +1656,44 @@ function viewItemObjectName(item) {
1857
1656
  const spec = item?.list ?? item;
1858
1657
  return spec?.data?.object ?? spec?.object ?? spec?.objectName;
1859
1658
  }
1659
+ var VIEW_OVERLAY_MARKER = "_isOverride";
1660
+ function isLegacyOverlayRow(item, spec) {
1661
+ if (spec && spec.config && typeof spec.config === "object") return false;
1662
+ const viewKind = item?.viewKind ?? spec?.viewKind;
1663
+ return viewKind === "list";
1664
+ }
1665
+ function isPersonalizationOverlayRow(item, spec) {
1666
+ if (item?.[VIEW_OVERLAY_MARKER] === true) return true;
1667
+ if (spec?.[VIEW_OVERLAY_MARKER] === true) return true;
1668
+ return isLegacyOverlayRow(item, spec);
1669
+ }
1670
+ var VIEW_OVERLAY_OWNED_KEYS = Object.freeze([
1671
+ "rowHeight",
1672
+ "sort",
1673
+ "hiddenFields",
1674
+ "columnState",
1675
+ "inlineEdit"
1676
+ ]);
1677
+ var VIEW_OVERLAY_IDENTITY_KEYS = Object.freeze([
1678
+ "name",
1679
+ "object",
1680
+ "viewKind",
1681
+ VIEW_OVERLAY_MARKER
1682
+ ]);
1683
+ function narrowPersonalizationOverlay(row) {
1684
+ if (!row || typeof row !== "object" || Array.isArray(row)) return row;
1685
+ const item = row;
1686
+ const spec = item.list ?? item;
1687
+ if (!isPersonalizationOverlayRow(item, spec)) return row;
1688
+ const narrowed = {};
1689
+ for (const key of VIEW_OVERLAY_IDENTITY_KEYS) {
1690
+ if (item[key] !== void 0) narrowed[key] = item[key];
1691
+ }
1692
+ for (const key of VIEW_OVERLAY_OWNED_KEYS) {
1693
+ if (item[key] !== void 0) narrowed[key] = item[key];
1694
+ }
1695
+ return narrowed;
1696
+ }
1860
1697
  function unwrapViewDraft(resp) {
1861
1698
  if (!resp || typeof resp !== "object") return null;
1862
1699
  const env = resp;
@@ -3426,25 +3263,59 @@ var ObjectStackAdapter = class {
3426
3263
  }
3427
3264
  }
3428
3265
  /**
3429
- * Persist a view definition for an object.
3266
+ * Persist a toolbar-driven view config patch — density, column widths,
3267
+ * sort, hidden columns, inline edit. Symmetric counterpart to
3268
+ * {@link getView}: writes the row to the server metadata store via
3269
+ * `client.meta.saveItem`, then invalidates the matching cache entry so the
3270
+ * next {@link getView} reflects the new payload. Returns the persisted item
3271
+ * when the server echoes it, otherwise undefined.
3272
+ *
3273
+ * Called from exactly ONE production site — `ObjectView`'s
3274
+ * `persistViewPatch`, for the toolbar toggle — but that ONE call site
3275
+ * fires for BOTH kinds of active tab: a code-defined **system** view (no
3276
+ * row of its own yet) and a genuinely user-created **saved** view (already
3277
+ * has a row — the toggle is editing ITS OWN definition, not laying an
3278
+ * overlay on top of it). Which one a given call means is NOT re-derived
3279
+ * here from the write's shape (objectui#4227's own lesson: shape inference
3280
+ * on this namespace is exactly what let a system view masquerade as
3281
+ * saved) — the caller already knows, via the same `isSavedViewId`
3282
+ * classification that gates the switcher's readonly flag and its five
3283
+ * mutating handlers, and passes it as {@link opts.isSavedView}.
3430
3284
  *
3431
- * Symmetric counterpart to {@link getView}: writes the view to the
3432
- * server metadata store via `client.meta.saveItem`, then invalidates
3433
- * the matching cache entry so the next {@link getView} reflects the
3434
- * new payload. Returns the persisted item when the server echoes it,
3435
- * otherwise undefined.
3285
+ * - `isSavedView` false/omitted (system-view target, the common case and
3286
+ * the default for backward compatibility): stamps
3287
+ * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
3288
+ * the original objectui#4227 fix.
3289
+ * - `isSavedView` true: the marker is withheld. Stamping it here would
3290
+ * flag the saved view's OWN row as a personalization overlay, and
3291
+ * `listViews()` would exclude it on the very next read — the user's own
3292
+ * view would vanish from the switcher the moment they toggled its
3293
+ * density (objectui#4227 follow-up, PM review on PR #4713, measured:
3294
+ * `persistViewPatch` has no gate on which kind of tab is active, and
3295
+ * this method writes to the exact same `(type='view', name=viewId)` key
3296
+ * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
3297
+ * that view, so the write is an upsert onto the saved view's row, not a
3298
+ * new one).
3436
3299
  *
3437
- * Used by ObjectView for "live" toolbar persistence (density,
3438
- * column widths, sort, etc.) and by the View Config Panel for
3439
- * explicit saves.
3300
+ * Per objectstack#7494's ruling, the overlay this writes (system-view
3301
+ * case) is ORG-WIDE shared view settings, not a per-user preference — a
3302
+ * true per-user scope is a parked v18 direction on the platform side.
3440
3303
  *
3441
3304
  * @param objectName - Object name (e.g. 'lead')
3442
3305
  * @param viewId - View identifier (e.g. 'all_leads')
3443
3306
  * @param config - Full view definition to persist
3307
+ * @param opts.isSavedView - Whether `viewId` already names a saved view
3308
+ * (vs. a system view being personalized for the first time). Omit /
3309
+ * `false` for the default overlay-marking behavior.
3444
3310
  */
3445
- async updateViewConfig(objectName, viewId, config) {
3311
+ async updateViewConfig(objectName, viewId, config, opts) {
3446
3312
  await this.connect();
3447
- const merged = { ...config || {}, object: config?.object || objectName, name: viewId };
3313
+ const merged = {
3314
+ ...config || {},
3315
+ object: config?.object || objectName,
3316
+ name: viewId,
3317
+ ...opts?.isSavedView ? {} : { [VIEW_OVERLAY_MARKER]: true }
3318
+ };
3448
3319
  const result = await this.client.meta.saveItem(
3449
3320
  "view",
3450
3321
  viewId,
@@ -3483,7 +3354,9 @@ var ObjectStackAdapter = class {
3483
3354
  const spec = v.list ?? v;
3484
3355
  if (viewItemObjectName(v) !== objectName) return false;
3485
3356
  const viewKind = v.viewKind ?? spec?.viewKind;
3486
- return !(viewKind && FORM_FAMILY.has(viewKind));
3357
+ if (viewKind && FORM_FAMILY.has(viewKind)) return false;
3358
+ if (isPersonalizationOverlayRow(v, spec)) return false;
3359
+ return true;
3487
3360
  }).map((v) => {
3488
3361
  const spec = v.list ?? v;
3489
3362
  const isDraft = v._draft === true || spec?._draft === true;
@@ -3758,7 +3631,7 @@ var ObjectStackAdapter = class {
3758
3631
  try {
3759
3632
  const cacheKey = `app:${appId}`;
3760
3633
  return await this.metadataCache.get(cacheKey, async () => {
3761
- const result = await this.client.meta.getItem("apps", appId);
3634
+ const result = await this.client.meta.getItem("app", appId);
3762
3635
  if (result && result.item) return result.item;
3763
3636
  return result ?? null;
3764
3637
  });
@@ -3770,8 +3643,13 @@ var ObjectStackAdapter = class {
3770
3643
  * Ask the by-name meta app route WHY an app is not in this session's app list
3771
3644
  * (objectui#4252).
3772
3645
  *
3773
- * `GET /api/v1/meta/apps` is filtered per session server-side
3774
- * (`filterAppForUser`), so an app withheld by `requiredPermissions` and an app
3646
+ * The app LIST is the generic metadata list route `GET /api/v1/meta/:type`,
3647
+ * requested with the singular type segment `app` — the same address this
3648
+ * method appends a name to below, and the one `MetadataProvider` reads its
3649
+ * items from. The server filters that list per session in `filterAppForUser`
3650
+ * (`packages/rest/src/rest-server.ts`, applied inside the `:type` list handler
3651
+ * once the type segment resolves to `app`), so an app withheld by
3652
+ * `requiredPermissions` and an app
3775
3653
  * that does not exist are byte-identical there: both are simply absent. A
3776
3654
  * console reading only that list has one fact and two conditions, and it
3777
3655
  * renders its copy for the wrong one — "it may still be publishing" over a
@@ -3821,7 +3699,7 @@ var ObjectStackAdapter = class {
3821
3699
  try {
3822
3700
  const cacheKey = `page:${pageId}`;
3823
3701
  return await this.metadataCache.get(cacheKey, async () => {
3824
- const result = await this.client.meta.getItem("pages", pageId);
3702
+ const result = await this.client.meta.getItem("page", pageId);
3825
3703
  if (result && result.item) return result.item;
3826
3704
  return result ?? null;
3827
3705
  });
@@ -3911,6 +3789,15 @@ var ObjectStackAdapter = class {
3911
3789
  if (failure.kind === "rejected") {
3912
3790
  throw new AnalyticsQueryRejectedError(failure.message, failure.code);
3913
3791
  }
3792
+ if (failure.kind === "unauthenticated") {
3793
+ throw new AnalyticsUnauthenticatedError({
3794
+ serverCode: failure.code,
3795
+ serverMessage: failure.message
3796
+ });
3797
+ }
3798
+ if (failure.kind === "cube-not-found") {
3799
+ throw e;
3800
+ }
3914
3801
  if (failure.kind === "not-installed") {
3915
3802
  this.warnAnalyticsCapabilityOnce(failure.message);
3916
3803
  }
@@ -3990,14 +3877,35 @@ var ObjectStackAdapter = class {
3990
3877
  body: JSON.stringify(requestBody)
3991
3878
  });
3992
3879
  if (!res.ok) {
3993
- let detail = "";
3880
+ let errBody;
3994
3881
  try {
3995
- const errBody = await res.json();
3996
- detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
3882
+ errBody = await res.json();
3997
3883
  } catch {
3998
3884
  }
3999
- if (res.status === 501 || res.status === 404) {
4000
- throw new AnalyticsNotInstalledError("POST /analytics/dataset/query", detail || void 0);
3885
+ const { code: serverCode, message: serverMessage } = readAnalyticsErrorEnvelope(errBody);
3886
+ const detail = serverMessage || (errBody === void 0 ? "" : JSON.stringify(errBody));
3887
+ const surface = "POST /analytics/dataset/query";
3888
+ const requestedDatasetName = typeof dataset === "string" ? dataset : typeof dataset?.name === "string" ? String(dataset.name) : void 0;
3889
+ if (errorCodeIsAnyOf({ code: serverCode }, ["NOT_IMPLEMENTED", "ROUTE_NOT_FOUND"])) {
3890
+ throw new AnalyticsNotInstalledError(surface, serverMessage, serverCode);
3891
+ }
3892
+ if (errorCodeIs({ code: serverCode }, "NOT_FOUND")) {
3893
+ throw new AnalyticsDatasetNotFoundError({
3894
+ datasetName: requestedDatasetName,
3895
+ serverCode,
3896
+ serverMessage
3897
+ });
3898
+ }
3899
+ if (errorCodeIs({ code: serverCode }, "UNAUTHENTICATED")) {
3900
+ throw new AnalyticsUnauthenticatedError({ serverCode, serverMessage });
3901
+ }
3902
+ if (serverCode === void 0) {
3903
+ if (ANALYTICS_ABSENT_STATUSES.includes(res.status)) {
3904
+ throw new AnalyticsNotInstalledError(surface, detail || void 0);
3905
+ }
3906
+ if (res.status === 401) {
3907
+ throw new AnalyticsUnauthenticatedError({ serverMessage });
3908
+ }
4001
3909
  }
4002
3910
  throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
4003
3911
  }
@@ -4257,31 +4165,29 @@ function createObjectStackAdapter(config) {
4257
4165
  0 && (module.exports = {
4258
4166
  API_ACCESS_DENIED_CODES,
4259
4167
  APP_PERMISSION_DENIED_CODE,
4168
+ AnalyticsDatasetNotFoundError,
4260
4169
  AnalyticsNotInstalledError,
4261
4170
  AnalyticsQueryRejectedError,
4171
+ AnalyticsUnauthenticatedError,
4262
4172
  AuthenticationError,
4263
4173
  BulkOperationError,
4264
4174
  ConcurrentUpdateError,
4265
4175
  ConnectionError,
4266
4176
  DataApiValidationError,
4267
4177
  FILTER_OPERATOR_ALIASES,
4268
- IntegrationManager,
4269
4178
  MalformedFilterError,
4270
4179
  MetadataClient,
4271
4180
  MetadataNotFoundError,
4272
4181
  ObjectStackAdapter,
4273
4182
  ObjectStackError,
4274
- SecurityManager,
4275
- calculateAutoLayout,
4183
+ VIEW_OVERLAY_OWNED_KEYS,
4276
4184
  classifyAnalyticsFailure,
4277
4185
  clearSharedDiscoveryCache,
4278
- createDefaultCanvasConfig,
4279
4186
  createErrorFromResponse,
4280
4187
  createObjectStackAdapter,
4281
4188
  createObjectStackUserStateAdapter,
4282
4189
  createQuietHttpLogger,
4283
4190
  formatHttpFailureMessage,
4284
- generateContractManifest,
4285
4191
  getSharedDiscovery,
4286
4192
  is404Error,
4287
4193
  isAnalyticsNotInstalledError,
@@ -4291,11 +4197,10 @@ function createObjectStackAdapter(config) {
4291
4197
  isErrorType,
4292
4198
  isMalformedFilterError,
4293
4199
  isObjectStackError,
4200
+ narrowPersonalizationOverlay,
4294
4201
  normaliseClientError,
4295
4202
  readSaveAdvisories,
4296
4203
  readTransactionalBatchCapability,
4297
4204
  serializeOrderBy,
4298
- snapToGrid,
4299
- validatePluginContract,
4300
4205
  viewItemObjectName
4301
4206
  });