@object-ui/data-objectstack 4.0.9 → 4.0.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 4.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@4.0.11
8
+ - @object-ui/core@4.0.11
9
+
10
+ ## 4.0.10
11
+
12
+ ### Patch Changes
13
+
14
+ - @object-ui/types@4.0.10
15
+ - @object-ui/core@4.0.10
16
+
3
17
  ## 4.0.9
4
18
 
5
19
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -962,6 +962,11 @@ var ObjectStackAdapter = class {
962
962
  // multiple sibling components requesting the same dataset on first paint)
963
963
  // into a single network round trip.
964
964
  __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
965
+ // Resources that have responded 404 at least once (collection not installed
966
+ // on this backend). Subsequent find() calls short-circuit to an empty result
967
+ // so optional collections like sys_presence don't hammer the server with
968
+ // failing requests on every record open / panel render.
969
+ __publicField(this, "missingResources", /* @__PURE__ */ new Set());
965
970
  this.client = new import_client.ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
966
971
  this.metadataCache = new MetadataCache(config.cache);
967
972
  this.autoReconnect = config.autoReconnect ?? true;
@@ -1099,18 +1104,29 @@ var ObjectStackAdapter = class {
1099
1104
  * Converts OData-style params to ObjectStack query options.
1100
1105
  */
1101
1106
  async find(resource, params) {
1107
+ if (this.missingResources.has(resource)) {
1108
+ return { data: [], total: 0 };
1109
+ }
1102
1110
  const key = `${resource}::${stableStringify(params)}`;
1103
1111
  const existing = this.inflightFinds.get(key);
1104
1112
  if (existing) return existing;
1105
1113
  const promise = (async () => {
1106
1114
  await this.connect();
1107
1115
  if (params?.$expand && params.$expand.length > 0) {
1108
- const result2 = await this.rawFindWithPopulate(resource, params);
1109
- return this.normalizeQueryResult(result2, params);
1116
+ const result = await this.rawFindWithPopulate(resource, params);
1117
+ return this.normalizeQueryResult(result, params);
1110
1118
  }
1111
1119
  const queryOptions = this.convertQueryParams(params);
1112
- const result = await this.client.data.find(resource, queryOptions);
1113
- return this.normalizeQueryResult(result, params);
1120
+ try {
1121
+ const result = await this.client.data.find(resource, queryOptions);
1122
+ return this.normalizeQueryResult(result, params);
1123
+ } catch (err) {
1124
+ if (is404Error(err)) {
1125
+ this.missingResources.add(resource);
1126
+ return { data: [], total: 0 };
1127
+ }
1128
+ throw err;
1129
+ }
1114
1130
  })();
1115
1131
  this.inflightFinds.set(key, promise);
1116
1132
  const cleanup = () => {
@@ -1586,20 +1602,129 @@ var ObjectStackAdapter = class {
1586
1602
  async updateViewConfig(objectName, viewId, config) {
1587
1603
  await this.connect();
1588
1604
  try {
1605
+ const merged = { ...config || {}, object: config?.object || objectName, name: viewId };
1589
1606
  const result = await this.client.meta.saveItem(
1590
- objectName,
1607
+ "view",
1591
1608
  viewId,
1592
- config
1609
+ merged
1593
1610
  );
1594
1611
  const cacheKey = `view:${objectName}:${viewId}`;
1595
1612
  this.metadataCache.invalidate?.(cacheKey);
1596
1613
  this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1614
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1597
1615
  if (result && result.item) return result.item;
1598
1616
  return result ?? void 0;
1599
1617
  } catch (err) {
1600
1618
  throw err;
1601
1619
  }
1602
1620
  }
1621
+ /**
1622
+ * List user-created views for a given object via the metadata overlay
1623
+ * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
1624
+ * that wrote to a physical `sys_view` table whose columns no longer
1625
+ * match the view spec shape.
1626
+ *
1627
+ * Returns view spec objects with their canonical `name` as identifier.
1628
+ * Filters by `data.object === objectName` (or top-level `object`)
1629
+ * client-side because the metadata index is name-only, not field-typed.
1630
+ */
1631
+ async listViews(objectName) {
1632
+ await this.connect();
1633
+ try {
1634
+ const result = await this.client.meta.getItems("view");
1635
+ const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
1636
+ return items.filter((v) => {
1637
+ if (!v) return false;
1638
+ const spec = v.list ?? v;
1639
+ const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
1640
+ return obj === objectName;
1641
+ }).map((v) => v.list ?? v);
1642
+ } catch (err) {
1643
+ console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
1644
+ return [];
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Create a new overlay view for an object. The view's `name` is the
1649
+ * stable identifier — must be unique within the project scope. Returns
1650
+ * the persisted view spec (or undefined when the server doesn't echo).
1651
+ *
1652
+ * Generates a snake_case name if `spec.name` is not provided by appending
1653
+ * a short timestamp suffix to the source-name hint.
1654
+ */
1655
+ async createView(objectName, spec) {
1656
+ await this.connect();
1657
+ let name = String(spec?.name || "").trim();
1658
+ if (!name) {
1659
+ let base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
1660
+ if (!base || /^[0-9]/.test(base)) {
1661
+ base = base ? `view_${base}` : "view";
1662
+ }
1663
+ const suffix = Date.now().toString(36);
1664
+ name = `${base}_${suffix}`;
1665
+ }
1666
+ const fullSpec = {
1667
+ ...spec,
1668
+ name,
1669
+ object: spec?.object || objectName,
1670
+ data: spec?.data || { provider: "object", object: objectName }
1671
+ };
1672
+ try {
1673
+ const result = await this.client.meta.saveItem("view", name, fullSpec);
1674
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1675
+ if (result && result.item) return result.item;
1676
+ return fullSpec;
1677
+ } catch (err) {
1678
+ throw err;
1679
+ }
1680
+ }
1681
+ /**
1682
+ * Apply a partial update to an existing overlay view. Reads the current
1683
+ * overlay (or seeds from artifact), merges, and writes back. ADR-0005
1684
+ * overlay rows store the *full* view document, so partial updates require
1685
+ * a read-merge-write cycle.
1686
+ */
1687
+ async updateView(objectName, viewName, partial) {
1688
+ await this.connect();
1689
+ let current = {};
1690
+ try {
1691
+ const r = await this.client.meta.getItem("view", viewName);
1692
+ current = r && (r.item || r) || {};
1693
+ if (current?.list) current = current.list;
1694
+ } catch {
1695
+ }
1696
+ const merged = {
1697
+ ...current,
1698
+ ...partial,
1699
+ name: viewName,
1700
+ object: current?.object || current?.data?.object || objectName
1701
+ };
1702
+ try {
1703
+ const result = await this.client.meta.saveItem("view", viewName, merged);
1704
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1705
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1706
+ if (result && result.item) return result.item;
1707
+ return merged;
1708
+ } catch (err) {
1709
+ throw err;
1710
+ }
1711
+ }
1712
+ /**
1713
+ * Delete an overlay view (reset to artifact default if one exists, or
1714
+ * remove entirely if it was a user-created view). Routes to
1715
+ * `DELETE /api/v1/meta/view/:name`.
1716
+ */
1717
+ async deleteView(objectName, viewName) {
1718
+ await this.connect();
1719
+ try {
1720
+ const result = await this.client.meta.deleteItem("view", viewName);
1721
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1722
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1723
+ return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
1724
+ } catch (err) {
1725
+ throw err;
1726
+ }
1727
+ }
1603
1728
  /**
1604
1729
  * Get an application definition by name or ID.
1605
1730
  * Attempts to fetch from the server metadata API.
package/dist/index.d.cts CHANGED
@@ -734,6 +734,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
734
734
  private token?;
735
735
  private fetchImpl;
736
736
  private inflightFinds;
737
+ private missingResources;
737
738
  constructor(config: {
738
739
  baseUrl: string;
739
740
  token?: string;
@@ -896,6 +897,41 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
896
897
  * @param config - Full view definition to persist
897
898
  */
898
899
  updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
900
+ /**
901
+ * List user-created views for a given object via the metadata overlay
902
+ * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
903
+ * that wrote to a physical `sys_view` table whose columns no longer
904
+ * match the view spec shape.
905
+ *
906
+ * Returns view spec objects with their canonical `name` as identifier.
907
+ * Filters by `data.object === objectName` (or top-level `object`)
908
+ * client-side because the metadata index is name-only, not field-typed.
909
+ */
910
+ listViews(objectName: string): Promise<any[]>;
911
+ /**
912
+ * Create a new overlay view for an object. The view's `name` is the
913
+ * stable identifier — must be unique within the project scope. Returns
914
+ * the persisted view spec (or undefined when the server doesn't echo).
915
+ *
916
+ * Generates a snake_case name if `spec.name` is not provided by appending
917
+ * a short timestamp suffix to the source-name hint.
918
+ */
919
+ createView(objectName: string, spec: Record<string, any>): Promise<Record<string, any> | void>;
920
+ /**
921
+ * Apply a partial update to an existing overlay view. Reads the current
922
+ * overlay (or seeds from artifact), merges, and writes back. ADR-0005
923
+ * overlay rows store the *full* view document, so partial updates require
924
+ * a read-merge-write cycle.
925
+ */
926
+ updateView(objectName: string, viewName: string, partial: Record<string, any>): Promise<Record<string, any> | void>;
927
+ /**
928
+ * Delete an overlay view (reset to artifact default if one exists, or
929
+ * remove entirely if it was a user-created view). Routes to
930
+ * `DELETE /api/v1/meta/view/:name`.
931
+ */
932
+ deleteView(objectName: string, viewName: string): Promise<{
933
+ deleted: boolean;
934
+ }>;
899
935
  /**
900
936
  * Get an application definition by name or ID.
901
937
  * Attempts to fetch from the server metadata API.
package/dist/index.d.ts CHANGED
@@ -734,6 +734,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
734
734
  private token?;
735
735
  private fetchImpl;
736
736
  private inflightFinds;
737
+ private missingResources;
737
738
  constructor(config: {
738
739
  baseUrl: string;
739
740
  token?: string;
@@ -896,6 +897,41 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
896
897
  * @param config - Full view definition to persist
897
898
  */
898
899
  updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
900
+ /**
901
+ * List user-created views for a given object via the metadata overlay
902
+ * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
903
+ * that wrote to a physical `sys_view` table whose columns no longer
904
+ * match the view spec shape.
905
+ *
906
+ * Returns view spec objects with their canonical `name` as identifier.
907
+ * Filters by `data.object === objectName` (or top-level `object`)
908
+ * client-side because the metadata index is name-only, not field-typed.
909
+ */
910
+ listViews(objectName: string): Promise<any[]>;
911
+ /**
912
+ * Create a new overlay view for an object. The view's `name` is the
913
+ * stable identifier — must be unique within the project scope. Returns
914
+ * the persisted view spec (or undefined when the server doesn't echo).
915
+ *
916
+ * Generates a snake_case name if `spec.name` is not provided by appending
917
+ * a short timestamp suffix to the source-name hint.
918
+ */
919
+ createView(objectName: string, spec: Record<string, any>): Promise<Record<string, any> | void>;
920
+ /**
921
+ * Apply a partial update to an existing overlay view. Reads the current
922
+ * overlay (or seeds from artifact), merges, and writes back. ADR-0005
923
+ * overlay rows store the *full* view document, so partial updates require
924
+ * a read-merge-write cycle.
925
+ */
926
+ updateView(objectName: string, viewName: string, partial: Record<string, any>): Promise<Record<string, any> | void>;
927
+ /**
928
+ * Delete an overlay view (reset to artifact default if one exists, or
929
+ * remove entirely if it was a user-created view). Routes to
930
+ * `DELETE /api/v1/meta/view/:name`.
931
+ */
932
+ deleteView(objectName: string, viewName: string): Promise<{
933
+ deleted: boolean;
934
+ }>;
899
935
  /**
900
936
  * Get an application definition by name or ID.
901
937
  * Attempts to fetch from the server metadata API.
package/dist/index.js CHANGED
@@ -919,6 +919,11 @@ var ObjectStackAdapter = class {
919
919
  // multiple sibling components requesting the same dataset on first paint)
920
920
  // into a single network round trip.
921
921
  __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
922
+ // Resources that have responded 404 at least once (collection not installed
923
+ // on this backend). Subsequent find() calls short-circuit to an empty result
924
+ // so optional collections like sys_presence don't hammer the server with
925
+ // failing requests on every record open / panel render.
926
+ __publicField(this, "missingResources", /* @__PURE__ */ new Set());
922
927
  this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
923
928
  this.metadataCache = new MetadataCache(config.cache);
924
929
  this.autoReconnect = config.autoReconnect ?? true;
@@ -1056,18 +1061,29 @@ var ObjectStackAdapter = class {
1056
1061
  * Converts OData-style params to ObjectStack query options.
1057
1062
  */
1058
1063
  async find(resource, params) {
1064
+ if (this.missingResources.has(resource)) {
1065
+ return { data: [], total: 0 };
1066
+ }
1059
1067
  const key = `${resource}::${stableStringify(params)}`;
1060
1068
  const existing = this.inflightFinds.get(key);
1061
1069
  if (existing) return existing;
1062
1070
  const promise = (async () => {
1063
1071
  await this.connect();
1064
1072
  if (params?.$expand && params.$expand.length > 0) {
1065
- const result2 = await this.rawFindWithPopulate(resource, params);
1066
- return this.normalizeQueryResult(result2, params);
1073
+ const result = await this.rawFindWithPopulate(resource, params);
1074
+ return this.normalizeQueryResult(result, params);
1067
1075
  }
1068
1076
  const queryOptions = this.convertQueryParams(params);
1069
- const result = await this.client.data.find(resource, queryOptions);
1070
- return this.normalizeQueryResult(result, params);
1077
+ try {
1078
+ const result = await this.client.data.find(resource, queryOptions);
1079
+ return this.normalizeQueryResult(result, params);
1080
+ } catch (err) {
1081
+ if (is404Error(err)) {
1082
+ this.missingResources.add(resource);
1083
+ return { data: [], total: 0 };
1084
+ }
1085
+ throw err;
1086
+ }
1071
1087
  })();
1072
1088
  this.inflightFinds.set(key, promise);
1073
1089
  const cleanup = () => {
@@ -1543,20 +1559,129 @@ var ObjectStackAdapter = class {
1543
1559
  async updateViewConfig(objectName, viewId, config) {
1544
1560
  await this.connect();
1545
1561
  try {
1562
+ const merged = { ...config || {}, object: config?.object || objectName, name: viewId };
1546
1563
  const result = await this.client.meta.saveItem(
1547
- objectName,
1564
+ "view",
1548
1565
  viewId,
1549
- config
1566
+ merged
1550
1567
  );
1551
1568
  const cacheKey = `view:${objectName}:${viewId}`;
1552
1569
  this.metadataCache.invalidate?.(cacheKey);
1553
1570
  this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1571
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1554
1572
  if (result && result.item) return result.item;
1555
1573
  return result ?? void 0;
1556
1574
  } catch (err) {
1557
1575
  throw err;
1558
1576
  }
1559
1577
  }
1578
+ /**
1579
+ * List user-created views for a given object via the metadata overlay
1580
+ * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
1581
+ * that wrote to a physical `sys_view` table whose columns no longer
1582
+ * match the view spec shape.
1583
+ *
1584
+ * Returns view spec objects with their canonical `name` as identifier.
1585
+ * Filters by `data.object === objectName` (or top-level `object`)
1586
+ * client-side because the metadata index is name-only, not field-typed.
1587
+ */
1588
+ async listViews(objectName) {
1589
+ await this.connect();
1590
+ try {
1591
+ const result = await this.client.meta.getItems("view");
1592
+ const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
1593
+ return items.filter((v) => {
1594
+ if (!v) return false;
1595
+ const spec = v.list ?? v;
1596
+ const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
1597
+ return obj === objectName;
1598
+ }).map((v) => v.list ?? v);
1599
+ } catch (err) {
1600
+ console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
1601
+ return [];
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Create a new overlay view for an object. The view's `name` is the
1606
+ * stable identifier — must be unique within the project scope. Returns
1607
+ * the persisted view spec (or undefined when the server doesn't echo).
1608
+ *
1609
+ * Generates a snake_case name if `spec.name` is not provided by appending
1610
+ * a short timestamp suffix to the source-name hint.
1611
+ */
1612
+ async createView(objectName, spec) {
1613
+ await this.connect();
1614
+ let name = String(spec?.name || "").trim();
1615
+ if (!name) {
1616
+ let base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
1617
+ if (!base || /^[0-9]/.test(base)) {
1618
+ base = base ? `view_${base}` : "view";
1619
+ }
1620
+ const suffix = Date.now().toString(36);
1621
+ name = `${base}_${suffix}`;
1622
+ }
1623
+ const fullSpec = {
1624
+ ...spec,
1625
+ name,
1626
+ object: spec?.object || objectName,
1627
+ data: spec?.data || { provider: "object", object: objectName }
1628
+ };
1629
+ try {
1630
+ const result = await this.client.meta.saveItem("view", name, fullSpec);
1631
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1632
+ if (result && result.item) return result.item;
1633
+ return fullSpec;
1634
+ } catch (err) {
1635
+ throw err;
1636
+ }
1637
+ }
1638
+ /**
1639
+ * Apply a partial update to an existing overlay view. Reads the current
1640
+ * overlay (or seeds from artifact), merges, and writes back. ADR-0005
1641
+ * overlay rows store the *full* view document, so partial updates require
1642
+ * a read-merge-write cycle.
1643
+ */
1644
+ async updateView(objectName, viewName, partial) {
1645
+ await this.connect();
1646
+ let current = {};
1647
+ try {
1648
+ const r = await this.client.meta.getItem("view", viewName);
1649
+ current = r && (r.item || r) || {};
1650
+ if (current?.list) current = current.list;
1651
+ } catch {
1652
+ }
1653
+ const merged = {
1654
+ ...current,
1655
+ ...partial,
1656
+ name: viewName,
1657
+ object: current?.object || current?.data?.object || objectName
1658
+ };
1659
+ try {
1660
+ const result = await this.client.meta.saveItem("view", viewName, merged);
1661
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1662
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1663
+ if (result && result.item) return result.item;
1664
+ return merged;
1665
+ } catch (err) {
1666
+ throw err;
1667
+ }
1668
+ }
1669
+ /**
1670
+ * Delete an overlay view (reset to artifact default if one exists, or
1671
+ * remove entirely if it was a user-created view). Routes to
1672
+ * `DELETE /api/v1/meta/view/:name`.
1673
+ */
1674
+ async deleteView(objectName, viewName) {
1675
+ await this.connect();
1676
+ try {
1677
+ const result = await this.client.meta.deleteItem("view", viewName);
1678
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1679
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1680
+ return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
1681
+ } catch (err) {
1682
+ throw err;
1683
+ }
1684
+ }
1560
1685
  /**
1561
1686
  * Get an application definition by name or ID.
1562
1687
  * Attempts to fetch from the server metadata API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "4.0.9",
3
+ "version": "4.0.11",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@objectstack/client": "^4.0.5",
26
- "@object-ui/core": "4.0.9",
27
- "@object-ui/types": "4.0.9"
26
+ "@object-ui/core": "4.0.11",
27
+ "@object-ui/types": "4.0.11"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
package/src/index.ts CHANGED
@@ -275,6 +275,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
275
275
  // multiple sibling components requesting the same dataset on first paint)
276
276
  // into a single network round trip.
277
277
  private inflightFinds = new Map<string, Promise<QueryResult<T>>>();
278
+ // Resources that have responded 404 at least once (collection not installed
279
+ // on this backend). Subsequent find() calls short-circuit to an empty result
280
+ // so optional collections like sys_presence don't hammer the server with
281
+ // failing requests on every record open / panel render.
282
+ private missingResources = new Set<string>();
278
283
 
279
284
  constructor(config: {
280
285
  baseUrl: string;
@@ -470,6 +475,12 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
470
475
  * Converts OData-style params to ObjectStack query options.
471
476
  */
472
477
  async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
478
+ // Short-circuit when this resource has previously responded 404 — the
479
+ // collection isn't installed on this backend. Callers (AppHeader,
480
+ // RecordDetailView, …) treat empty data as "feature unavailable".
481
+ if (this.missingResources.has(resource)) {
482
+ return { data: [], total: 0 } as QueryResult<T>;
483
+ }
473
484
  const key = `${resource}::${stableStringify(params)}`;
474
485
  const existing = this.inflightFinds.get(key);
475
486
  if (existing) return existing;
@@ -489,8 +500,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
489
500
  }
490
501
 
491
502
  const queryOptions = this.convertQueryParams(params);
492
- const result: unknown = await this.client.data.find<T>(resource, queryOptions);
493
- return this.normalizeQueryResult(result, params);
503
+ try {
504
+ const result: unknown = await this.client.data.find<T>(resource, queryOptions);
505
+ return this.normalizeQueryResult(result, params);
506
+ } catch (err) {
507
+ if (is404Error(err)) {
508
+ // Mark the resource so subsequent calls don't repeat the 404.
509
+ this.missingResources.add(resource);
510
+ return { data: [], total: 0 } as QueryResult<T>;
511
+ }
512
+ throw err;
513
+ }
494
514
  })();
495
515
 
496
516
  this.inflightFinds.set(key, promise);
@@ -1116,16 +1136,23 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1116
1136
  ): Promise<Record<string, any> | void> {
1117
1137
  await this.connect();
1118
1138
  try {
1139
+ // ADR-0005 metadata customization overlay: persist views under
1140
+ // `type='view'` (NOT `type=<objectName>` — that was a pre-overlay
1141
+ // misuse that hit `/api/v1/meta/<objectName>/<viewId>`, which the
1142
+ // server never wired). The view's `data.object` field is what
1143
+ // associates it back to the object on read.
1144
+ const merged = { ...(config || {}), object: (config as any)?.object || objectName, name: viewId };
1119
1145
  const result: any = await this.client.meta.saveItem(
1120
- objectName,
1146
+ 'view',
1121
1147
  viewId,
1122
- config
1148
+ merged
1123
1149
  );
1124
1150
  // Invalidate cached read so next getView reflects the change
1125
1151
  const cacheKey = `view:${objectName}:${viewId}`;
1126
1152
  this.metadataCache.invalidate?.(cacheKey);
1127
1153
  // Also invalidate the batch override map so listViewOverrides re-fetches
1128
1154
  this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1155
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1129
1156
  if (result && result.item) return result.item;
1130
1157
  return result ?? undefined;
1131
1158
  } catch (err) {
@@ -1136,6 +1163,140 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1136
1163
  }
1137
1164
  }
1138
1165
 
1166
+ /**
1167
+ * List user-created views for a given object via the metadata overlay
1168
+ * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
1169
+ * that wrote to a physical `sys_view` table whose columns no longer
1170
+ * match the view spec shape.
1171
+ *
1172
+ * Returns view spec objects with their canonical `name` as identifier.
1173
+ * Filters by `data.object === objectName` (or top-level `object`)
1174
+ * client-side because the metadata index is name-only, not field-typed.
1175
+ */
1176
+ async listViews(objectName: string): Promise<any[]> {
1177
+ await this.connect();
1178
+ try {
1179
+ const result: any = await this.client.meta.getItems('view');
1180
+ const items: any[] = Array.isArray(result?.items)
1181
+ ? result.items
1182
+ : Array.isArray(result) ? result : [];
1183
+ return items.filter((v: any) => {
1184
+ if (!v) return false;
1185
+ // Handle both bare view spec and `{list: {...}}` artifact wrapper
1186
+ const spec = v.list ?? v;
1187
+ const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
1188
+ return obj === objectName;
1189
+ }).map((v: any) => v.list ?? v);
1190
+ } catch (err) {
1191
+ console.warn('[OBJECTSTACKDataSource] listViews failed:', err);
1192
+ return [];
1193
+ }
1194
+ }
1195
+
1196
+ /**
1197
+ * Create a new overlay view for an object. The view's `name` is the
1198
+ * stable identifier — must be unique within the project scope. Returns
1199
+ * the persisted view spec (or undefined when the server doesn't echo).
1200
+ *
1201
+ * Generates a snake_case name if `spec.name` is not provided by appending
1202
+ * a short timestamp suffix to the source-name hint.
1203
+ */
1204
+ async createView(
1205
+ objectName: string,
1206
+ spec: Record<string, any>,
1207
+ ): Promise<Record<string, any> | void> {
1208
+ await this.connect();
1209
+ let name = String(spec?.name || '').trim();
1210
+ if (!name) {
1211
+ let base = String(spec?.label || objectName || 'view')
1212
+ .toLowerCase()
1213
+ .replace(/[^a-z0-9_]+/g, '_')
1214
+ .replace(/^_+|_+$/g, '')
1215
+ .slice(0, 40);
1216
+ // Spec requires snake_case starting with a letter or underscore.
1217
+ // Labels like "表格 1" collapse to "1" after non-ascii stripping, so we
1218
+ // need a fallback / prefix to keep the identifier valid.
1219
+ if (!base || /^[0-9]/.test(base)) {
1220
+ base = base ? `view_${base}` : 'view';
1221
+ }
1222
+ const suffix = Date.now().toString(36);
1223
+ name = `${base}_${suffix}`;
1224
+ }
1225
+ const fullSpec = {
1226
+ ...spec,
1227
+ name,
1228
+ object: spec?.object || objectName,
1229
+ data: spec?.data || { provider: 'object', object: objectName },
1230
+ };
1231
+ try {
1232
+ const result: any = await this.client.meta.saveItem('view', name, fullSpec);
1233
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1234
+ if (result && result.item) return result.item;
1235
+ return fullSpec;
1236
+ } catch (err) {
1237
+ throw err;
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * Apply a partial update to an existing overlay view. Reads the current
1243
+ * overlay (or seeds from artifact), merges, and writes back. ADR-0005
1244
+ * overlay rows store the *full* view document, so partial updates require
1245
+ * a read-merge-write cycle.
1246
+ */
1247
+ async updateView(
1248
+ objectName: string,
1249
+ viewName: string,
1250
+ partial: Record<string, any>,
1251
+ ): Promise<Record<string, any> | void> {
1252
+ await this.connect();
1253
+ let current: any = {};
1254
+ try {
1255
+ const r: any = await this.client.meta.getItem('view', viewName);
1256
+ current = (r && (r.item || r)) || {};
1257
+ // Some endpoints return the bare item; others wrap as {type,name,item}
1258
+ if (current?.list) current = current.list;
1259
+ } catch {
1260
+ // Treat missing as create-equivalent
1261
+ }
1262
+ const merged = {
1263
+ ...current,
1264
+ ...partial,
1265
+ name: viewName,
1266
+ object: current?.object || (current as any)?.data?.object || objectName,
1267
+ };
1268
+ try {
1269
+ const result: any = await this.client.meta.saveItem('view', viewName, merged);
1270
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1271
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1272
+ if (result && result.item) return result.item;
1273
+ return merged;
1274
+ } catch (err) {
1275
+ throw err;
1276
+ }
1277
+ }
1278
+
1279
+ /**
1280
+ * Delete an overlay view (reset to artifact default if one exists, or
1281
+ * remove entirely if it was a user-created view). Routes to
1282
+ * `DELETE /api/v1/meta/view/:name`.
1283
+ */
1284
+ async deleteView(
1285
+ objectName: string,
1286
+ viewName: string,
1287
+ ): Promise<{ deleted: boolean }> {
1288
+ await this.connect();
1289
+ try {
1290
+ const result: any = await this.client.meta.deleteItem('view', viewName);
1291
+ this.metadataCache.invalidate?.(`views:${objectName}`);
1292
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
1293
+ return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
1294
+ } catch (err) {
1295
+ throw err;
1296
+ }
1297
+ }
1298
+
1299
+
1139
1300
  /**
1140
1301
  * Get an application definition by name or ID.
1141
1302
  * Attempts to fetch from the server metadata API.