@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 +14 -0
- package/dist/index.cjs +131 -6
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +131 -6
- package/package.json +3 -3
- package/src/index.ts +165 -4
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
|
|
1109
|
-
return this.normalizeQueryResult(
|
|
1116
|
+
const result = await this.rawFindWithPopulate(resource, params);
|
|
1117
|
+
return this.normalizeQueryResult(result, params);
|
|
1110
1118
|
}
|
|
1111
1119
|
const queryOptions = this.convertQueryParams(params);
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1607
|
+
"view",
|
|
1591
1608
|
viewId,
|
|
1592
|
-
|
|
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
|
|
1066
|
-
return this.normalizeQueryResult(
|
|
1073
|
+
const result = await this.rawFindWithPopulate(resource, params);
|
|
1074
|
+
return this.normalizeQueryResult(result, params);
|
|
1067
1075
|
}
|
|
1068
1076
|
const queryOptions = this.convertQueryParams(params);
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1564
|
+
"view",
|
|
1548
1565
|
viewId,
|
|
1549
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"@object-ui/types": "4.0.
|
|
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
|
-
|
|
493
|
-
|
|
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
|
-
|
|
1146
|
+
'view',
|
|
1121
1147
|
viewId,
|
|
1122
|
-
|
|
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.
|