@object-ui/data-objectstack 17.1.0 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +228 -0
- package/README.md +6 -1
- package/dist/index.cjs +56 -14
- package/dist/index.d.cts +66 -28
- package/dist/index.d.ts +66 -28
- package/dist/index.js +55 -13
- package/package.json +5 -5
- package/src/cache/MetadataCache.ts +12 -3
- package/src/errors.test.ts +12 -12
- package/src/errors.ts +23 -5
- package/src/index.ts +110 -21
- package/src/metadata-client.ts +17 -3
- package/src/onWriteWarning.test.ts +101 -0
- package/src/security.ts +18 -5
- package/src/spec-symbol-batch6.test.ts +306 -0
- package/src/validation-error.test.ts +9 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ObjectStackClient } from '@objectstack/client';
|
|
2
|
+
import { DroppedFieldsEvent } from '@objectstack/spec/data';
|
|
3
|
+
export { DroppedFieldsEvent } from '@objectstack/spec/data';
|
|
2
4
|
import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, FileUploadResult } from '@object-ui/types';
|
|
3
5
|
export { FileUploadResult } from '@object-ui/types';
|
|
4
6
|
|
|
@@ -95,16 +97,28 @@ declare class AuthenticationError extends ObjectStackError {
|
|
|
95
97
|
constructor(message?: string, details?: Record<string, unknown>, statusCode?: number);
|
|
96
98
|
}
|
|
97
99
|
/**
|
|
98
|
-
* Error thrown when data
|
|
100
|
+
* Error thrown when the ObjectStack data API rejects a write as invalid.
|
|
101
|
+
*
|
|
102
|
+
* NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
|
|
103
|
+
* class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
|
|
104
|
+
* a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
|
|
105
|
+
* manifest's validation report — and `@object-ui/types` re-exports it under that
|
|
106
|
+
* name. This is a runtime `Error` subclass carrying an HTTP status plus a list
|
|
107
|
+
* of such entries, so the two are not even the same KIND of thing.
|
|
108
|
+
*
|
|
109
|
+
* The name follows the convention registered on objectstack#4115 for this
|
|
110
|
+
* family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
|
|
111
|
+
* took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
|
|
112
|
+
* the data API.
|
|
99
113
|
*/
|
|
100
|
-
declare class
|
|
114
|
+
declare class DataApiValidationError extends ObjectStackError {
|
|
101
115
|
field?: string | undefined;
|
|
102
116
|
validationErrors?: Array<{
|
|
103
117
|
field: string;
|
|
104
118
|
message: string;
|
|
105
119
|
}> | undefined;
|
|
106
120
|
/**
|
|
107
|
-
* Create a new
|
|
121
|
+
* Create a new DataApiValidationError
|
|
108
122
|
*
|
|
109
123
|
* @param message - Human-readable error message
|
|
110
124
|
* @param field - The field that failed validation (optional)
|
|
@@ -148,9 +162,18 @@ declare function isErrorType<T extends ObjectStackError>(error: unknown, errorCl
|
|
|
148
162
|
* LICENSE file in the root directory of this source tree.
|
|
149
163
|
*/
|
|
150
164
|
/**
|
|
151
|
-
*
|
|
165
|
+
* Statistics reported by {@link MetadataCache.getStats} for monitoring.
|
|
166
|
+
*
|
|
167
|
+
* NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
|
|
168
|
+
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
|
|
169
|
+
* describes the platform's `ICacheService` — a server-side KV cache measured by
|
|
170
|
+
* `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
|
|
171
|
+
* of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
|
|
172
|
+
* concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
|
|
173
|
+
* Neither type has a key the other has, so this is a name collision, not a
|
|
174
|
+
* dialect — deriving would have replaced every field.
|
|
152
175
|
*/
|
|
153
|
-
interface
|
|
176
|
+
interface MetadataCacheStats {
|
|
154
177
|
size: number;
|
|
155
178
|
maxSize: number;
|
|
156
179
|
hits: number;
|
|
@@ -563,7 +586,21 @@ interface MetadataDraftHeader {
|
|
|
563
586
|
updatedAt: string | null;
|
|
564
587
|
updatedBy: string | null;
|
|
565
588
|
}
|
|
566
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
|
|
591
|
+
* `/api/v1/meta/:type/:name`.
|
|
592
|
+
*
|
|
593
|
+
* NOT the spec's `MetadataSaveOptions` (`@objectstack/spec/system` and
|
|
594
|
+
* `/kernel`), whose name this interface wore until objectui#3160
|
|
595
|
+
* (objectstack#4115 ledger batch 6). Both spec copies describe writing a
|
|
596
|
+
* metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
|
|
597
|
+
* `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
|
|
598
|
+
* exists here, and not one of these exists there: this is the REST client's
|
|
599
|
+
* request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
|
|
600
|
+
* attribution, the destructive-change override, the ADR-0033 draft/publish
|
|
601
|
+
* mode, and the owning package. Same words, different layer.
|
|
602
|
+
*/
|
|
603
|
+
interface MetadataClientSaveOptions {
|
|
567
604
|
/**
|
|
568
605
|
* Optimistic concurrency token (the `checksum` returned by the last
|
|
569
606
|
* read). When present, sent as the `If-Match` header so concurrent
|
|
@@ -608,7 +645,7 @@ interface MetadataGetOptions {
|
|
|
608
645
|
*/
|
|
609
646
|
packageId?: string;
|
|
610
647
|
}
|
|
611
|
-
interface MetadataDeleteOptions extends
|
|
648
|
+
interface MetadataDeleteOptions extends MetadataClientSaveOptions {
|
|
612
649
|
/**
|
|
613
650
|
* Target state. `'draft'` discards the pending draft (keeps the
|
|
614
651
|
* published overlay intact). Omit to reset the active overlay back
|
|
@@ -847,7 +884,7 @@ declare class MetadataClient {
|
|
|
847
884
|
* envelopes; we send bare for consistency. Pass `mode: 'draft'` to
|
|
848
885
|
* stage the change without publishing (Studio's "Save" button).
|
|
849
886
|
*/
|
|
850
|
-
save<T = unknown>(type: string, name: string, item: unknown, options?:
|
|
887
|
+
save<T = unknown>(type: string, name: string, item: unknown, options?: MetadataClientSaveOptions): Promise<T>;
|
|
851
888
|
/**
|
|
852
889
|
* Publish a single pending draft BY REFERENCE — promotes the draft row for
|
|
853
890
|
* `(type, name)` into the active overlay and drops the draft. Works for ANY
|
|
@@ -957,7 +994,20 @@ declare class MetadataClient {
|
|
|
957
994
|
* Security module integration for @objectstack/spec v3.0.0
|
|
958
995
|
* Provides advanced security policies: CSP config, audit logging, data masking.
|
|
959
996
|
*/
|
|
960
|
-
|
|
997
|
+
/**
|
|
998
|
+
* Configuration for {@link SecurityManager} — this adapter's browser-side
|
|
999
|
+
* security posture: the CSP header it generates, the in-memory audit log it
|
|
1000
|
+
* keeps, and the field-masking rules it applies before a record is rendered.
|
|
1001
|
+
*
|
|
1002
|
+
* NOT the spec's `SecurityPolicy` (`@objectstack/spec/kernel`), whose name this
|
|
1003
|
+
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
|
|
1004
|
+
* is the PACKAGE SUPPLY-CHAIN policy — `{ id, name, autoScan, thresholds,
|
|
1005
|
+
* allowedLicenses, prohibitedLicenses, codeSigning, sandbox }` — the rules a
|
|
1006
|
+
* marketplace applies when scanning a plugin before install. It shares no key
|
|
1007
|
+
* with this one, so there was nothing to derive; the collision is the word
|
|
1008
|
+
* "security", not the concept.
|
|
1009
|
+
*/
|
|
1010
|
+
interface SecurityManagerPolicy {
|
|
961
1011
|
/** Content Security Policy configuration */
|
|
962
1012
|
csp?: CSPConfig;
|
|
963
1013
|
/** Audit logging configuration */
|
|
@@ -1031,7 +1081,7 @@ interface AuditLogEntry {
|
|
|
1031
1081
|
declare class SecurityManager {
|
|
1032
1082
|
private policy;
|
|
1033
1083
|
private auditLog;
|
|
1034
|
-
constructor(policy?:
|
|
1084
|
+
constructor(policy?: SecurityManagerPolicy);
|
|
1035
1085
|
/**
|
|
1036
1086
|
* Generate a CSP header string from the configuration.
|
|
1037
1087
|
*/
|
|
@@ -1055,11 +1105,11 @@ declare class SecurityManager {
|
|
|
1055
1105
|
/**
|
|
1056
1106
|
* Update the security policy.
|
|
1057
1107
|
*/
|
|
1058
|
-
updatePolicy(policy: Partial<
|
|
1108
|
+
updatePolicy(policy: Partial<SecurityManagerPolicy>): void;
|
|
1059
1109
|
/**
|
|
1060
1110
|
* Get current security policy.
|
|
1061
1111
|
*/
|
|
1062
|
-
getPolicy():
|
|
1112
|
+
getPolicy(): SecurityManagerPolicy;
|
|
1063
1113
|
}
|
|
1064
1114
|
|
|
1065
1115
|
/**
|
|
@@ -1379,7 +1429,7 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
|
|
|
1379
1429
|
*
|
|
1380
1430
|
* Two shapes are recognised:
|
|
1381
1431
|
* - `409` + `CONCURRENT_UPDATE` → {@link ConcurrentUpdateError};
|
|
1382
|
-
* - `400` + `VALIDATION_FAILED` → {@link
|
|
1432
|
+
* - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
|
|
1383
1433
|
* server's per-field entries so a form can mark the offending inputs
|
|
1384
1434
|
* instead of showing one undirected toast.
|
|
1385
1435
|
*/
|
|
@@ -1414,19 +1464,7 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
|
|
|
1414
1464
|
* Event listener type for batch operation progress
|
|
1415
1465
|
*/
|
|
1416
1466
|
type BatchProgressListener = (event: BatchProgressEvent) => void;
|
|
1417
|
-
|
|
1418
|
-
* One server-reported write-strip: caller-supplied fields the backend LEGALLY
|
|
1419
|
-
* removed from a write before persisting (a non-system caller cannot seed a
|
|
1420
|
-
* `readonly` field, a `readonlyWhen` predicate locked it, etc.). Mirrors the
|
|
1421
|
-
* framework `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally
|
|
1422
|
-
* so we don't pin a client type version — `reason` is kept as a widened string
|
|
1423
|
-
* for forward-compatibility with reasons added server-side.
|
|
1424
|
-
*/
|
|
1425
|
-
interface DroppedFieldsEvent {
|
|
1426
|
-
object: string;
|
|
1427
|
-
fields: string[];
|
|
1428
|
-
reason: string;
|
|
1429
|
-
}
|
|
1467
|
+
|
|
1430
1468
|
/**
|
|
1431
1469
|
* Emitted after a create/update whose response carried `droppedFields`
|
|
1432
1470
|
* (framework #3431/#3455). The write SUCCEEDED — this is a warning that some
|
|
@@ -2085,7 +2123,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2085
2123
|
/**
|
|
2086
2124
|
* Get cache statistics for monitoring performance.
|
|
2087
2125
|
*/
|
|
2088
|
-
getCacheStats():
|
|
2126
|
+
getCacheStats(): MetadataCacheStats;
|
|
2089
2127
|
/**
|
|
2090
2128
|
* Invalidate metadata cache entries.
|
|
2091
2129
|
*
|
|
@@ -2173,4 +2211,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
|
|
|
2173
2211
|
reconnectDelay?: number;
|
|
2174
2212
|
}): DataSource<T>;
|
|
2175
2213
|
|
|
2176
|
-
export { AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type
|
|
2214
|
+
export { AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, DataApiValidationError, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, FILTER_OPERATOR_ALIASES, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityManagerPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, type WebhookIntegrationConfig, type WriteWarningEvent, type WriteWarningListener, calculateAutoLayout, classifyAnalyticsFailure, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, normaliseClientError, readTransactionalBatchCapability, serializeOrderBy, snapToGrid, validatePluginContract };
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ function errorCodeIsAnyOf(err, codes) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ../core/src/utils/filter-converter.ts
|
|
26
|
+
import { normalizeFilterOperator } from "@objectstack/spec/ui";
|
|
26
27
|
var FilterOperatorError = class extends Error {
|
|
27
28
|
constructor(message) {
|
|
28
29
|
super(message);
|
|
@@ -880,10 +881,10 @@ var AuthenticationError = class extends ObjectStackError {
|
|
|
880
881
|
this.name = "AuthenticationError";
|
|
881
882
|
}
|
|
882
883
|
};
|
|
883
|
-
var
|
|
884
|
+
var DataApiValidationError = class extends ObjectStackError {
|
|
884
885
|
/**
|
|
885
|
-
* Create a new
|
|
886
|
-
*
|
|
886
|
+
* Create a new DataApiValidationError
|
|
887
|
+
*
|
|
887
888
|
* @param message - Human-readable error message
|
|
888
889
|
* @param field - The field that failed validation (optional)
|
|
889
890
|
* @param validationErrors - Array of validation error details
|
|
@@ -934,7 +935,7 @@ function createErrorFromResponse(response, context) {
|
|
|
934
935
|
}
|
|
935
936
|
return new ObjectStackError(message, "NOT_FOUND", 404, details);
|
|
936
937
|
case 400:
|
|
937
|
-
return new
|
|
938
|
+
return new DataApiValidationError(message, void 0, response?.data?.errors, details);
|
|
938
939
|
case 503:
|
|
939
940
|
return new ConnectionError(message, response?.config?.url, details, 503);
|
|
940
941
|
case 504:
|
|
@@ -1451,7 +1452,7 @@ var FILTER_OPERATOR_ALIASES = {
|
|
|
1451
1452
|
before: "<",
|
|
1452
1453
|
after: ">"
|
|
1453
1454
|
};
|
|
1454
|
-
function
|
|
1455
|
+
function normalizeFilterOperator2(op) {
|
|
1455
1456
|
if (typeof op !== "string") return null;
|
|
1456
1457
|
const lower = op.toLowerCase();
|
|
1457
1458
|
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
@@ -1480,7 +1481,7 @@ function objectFilterEntryToAST(entry) {
|
|
|
1480
1481
|
if (!entry || typeof entry !== "object") return null;
|
|
1481
1482
|
const field = entry.field;
|
|
1482
1483
|
const rawOp = entry.operator ?? entry.op ?? "=";
|
|
1483
|
-
const op =
|
|
1484
|
+
const op = normalizeFilterOperator2(rawOp);
|
|
1484
1485
|
if (!field || !op) return null;
|
|
1485
1486
|
return [String(field), op, entry.value];
|
|
1486
1487
|
}
|
|
@@ -1647,7 +1648,7 @@ function normaliseClientError(error) {
|
|
|
1647
1648
|
const message = typeof rec.message === "string" && rec.message.trim() ? rec.message : typeof rec.code === "string" ? rec.code : "";
|
|
1648
1649
|
return { field, message };
|
|
1649
1650
|
}).filter((x) => x !== null);
|
|
1650
|
-
return new
|
|
1651
|
+
return new DataApiValidationError(
|
|
1651
1652
|
typeof e.message === "string" ? e.message : "Validation failed",
|
|
1652
1653
|
validationErrors[0]?.field,
|
|
1653
1654
|
validationErrors,
|
|
@@ -1698,6 +1699,31 @@ function stableStringify(value) {
|
|
|
1698
1699
|
const keys = Object.keys(obj).sort();
|
|
1699
1700
|
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
1700
1701
|
}
|
|
1702
|
+
function sameWireValue(a, b) {
|
|
1703
|
+
if (a === b) return true;
|
|
1704
|
+
const aEmpty = a === null || a === void 0;
|
|
1705
|
+
const bEmpty = b === null || b === void 0;
|
|
1706
|
+
if (aEmpty || bEmpty) return aEmpty && bEmpty;
|
|
1707
|
+
return stableStringify(a) === stableStringify(b);
|
|
1708
|
+
}
|
|
1709
|
+
function withoutNoOpDrops(droppedFields, sent, stored) {
|
|
1710
|
+
if (!sent || !stored || typeof sent !== "object" || typeof stored !== "object") {
|
|
1711
|
+
return droppedFields;
|
|
1712
|
+
}
|
|
1713
|
+
const out = [];
|
|
1714
|
+
for (const e of droppedFields) {
|
|
1715
|
+
const kept = e.fields.filter((f) => {
|
|
1716
|
+
if (!Object.prototype.hasOwnProperty.call(sent, f)) return true;
|
|
1717
|
+
if (!Object.prototype.hasOwnProperty.call(stored, f)) return true;
|
|
1718
|
+
return !sameWireValue(
|
|
1719
|
+
sent[f],
|
|
1720
|
+
stored[f]
|
|
1721
|
+
);
|
|
1722
|
+
});
|
|
1723
|
+
if (kept.length > 0) out.push({ ...e, fields: kept });
|
|
1724
|
+
}
|
|
1725
|
+
return out;
|
|
1726
|
+
}
|
|
1701
1727
|
var ObjectStackAdapter = class {
|
|
1702
1728
|
constructor(config) {
|
|
1703
1729
|
__publicField(this, "client");
|
|
@@ -2074,12 +2100,14 @@ var ObjectStackAdapter = class {
|
|
|
2074
2100
|
* whose response type predates `droppedFields`: the field is read structurally
|
|
2075
2101
|
* and validated, so an older client (or a backend that never drops) is a no-op.
|
|
2076
2102
|
*/
|
|
2077
|
-
notifyDroppedFields(operation, resource, result, id) {
|
|
2103
|
+
notifyDroppedFields(operation, resource, result, id, sent) {
|
|
2078
2104
|
const dropped = result?.droppedFields;
|
|
2079
2105
|
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2080
|
-
const
|
|
2106
|
+
const valid = dropped.filter(
|
|
2081
2107
|
(e) => !!e && typeof e === "object" && Array.isArray(e.fields) && e.fields.length > 0
|
|
2082
2108
|
);
|
|
2109
|
+
const stored = result?.record;
|
|
2110
|
+
const droppedFields = withoutNoOpDrops(valid, sent, stored);
|
|
2083
2111
|
if (droppedFields.length === 0) return;
|
|
2084
2112
|
this.emitWriteWarning({ operation, resource, ...id !== void 0 ? { id } : {}, droppedFields });
|
|
2085
2113
|
}
|
|
@@ -2098,17 +2126,25 @@ var ObjectStackAdapter = class {
|
|
|
2098
2126
|
notifyBatchDroppedFields(operations, payload) {
|
|
2099
2127
|
const dropped = payload?.droppedFields;
|
|
2100
2128
|
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2129
|
+
const results = payload?.results;
|
|
2101
2130
|
for (const entry of dropped) {
|
|
2102
2131
|
if (!entry || typeof entry !== "object") continue;
|
|
2103
2132
|
const e = entry;
|
|
2104
2133
|
if (!Array.isArray(e.fields) || e.fields.length === 0) continue;
|
|
2105
2134
|
const op = typeof e.index === "number" ? operations[e.index] : void 0;
|
|
2135
|
+
const stored = typeof e.index === "number" && Array.isArray(results) ? results[e.index] : void 0;
|
|
2136
|
+
const [live] = withoutNoOpDrops(
|
|
2137
|
+
[{ object: e.object, fields: e.fields, reason: e.reason }],
|
|
2138
|
+
op?.data,
|
|
2139
|
+
stored
|
|
2140
|
+
);
|
|
2141
|
+
if (!live) continue;
|
|
2106
2142
|
const operation = (op?.action ?? "create") === "create" ? "create" : "update";
|
|
2107
2143
|
this.emitWriteWarning({
|
|
2108
2144
|
operation,
|
|
2109
2145
|
resource: e.object ?? op?.object ?? "",
|
|
2110
2146
|
...op?.id !== void 0 && op?.id !== null ? { id: op.id } : {},
|
|
2111
|
-
droppedFields: [
|
|
2147
|
+
droppedFields: [live]
|
|
2112
2148
|
});
|
|
2113
2149
|
}
|
|
2114
2150
|
}
|
|
@@ -2128,7 +2164,13 @@ var ObjectStackAdapter = class {
|
|
|
2128
2164
|
try {
|
|
2129
2165
|
const result = await this.client.data.create(resource, data);
|
|
2130
2166
|
this.emitMutation({ type: "create", resource, record: { ...result.record } });
|
|
2131
|
-
this.notifyDroppedFields(
|
|
2167
|
+
this.notifyDroppedFields(
|
|
2168
|
+
"create",
|
|
2169
|
+
resource,
|
|
2170
|
+
result,
|
|
2171
|
+
result.record?.id,
|
|
2172
|
+
data
|
|
2173
|
+
);
|
|
2132
2174
|
return result.record;
|
|
2133
2175
|
} catch (err) {
|
|
2134
2176
|
throw normaliseClientError(err);
|
|
@@ -2155,7 +2197,7 @@ var ObjectStackAdapter = class {
|
|
|
2155
2197
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
2156
2198
|
);
|
|
2157
2199
|
this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
|
|
2158
|
-
this.notifyDroppedFields("update", resource, result, id);
|
|
2200
|
+
this.notifyDroppedFields("update", resource, result, id, data);
|
|
2159
2201
|
return result.record;
|
|
2160
2202
|
} catch (err) {
|
|
2161
2203
|
throw normaliseClientError(err);
|
|
@@ -3682,6 +3724,7 @@ export {
|
|
|
3682
3724
|
CloudOperations,
|
|
3683
3725
|
ConcurrentUpdateError,
|
|
3684
3726
|
ConnectionError,
|
|
3727
|
+
DataApiValidationError,
|
|
3685
3728
|
FILTER_OPERATOR_ALIASES,
|
|
3686
3729
|
IntegrationManager,
|
|
3687
3730
|
MalformedFilterError,
|
|
@@ -3690,7 +3733,6 @@ export {
|
|
|
3690
3733
|
ObjectStackAdapter,
|
|
3691
3734
|
ObjectStackError,
|
|
3692
3735
|
SecurityManager,
|
|
3693
|
-
ValidationError,
|
|
3694
3736
|
calculateAutoLayout,
|
|
3695
3737
|
classifyAnalyticsFailure,
|
|
3696
3738
|
clearSharedDiscoveryCache,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.3.0",
|
|
4
4
|
"description": "ObjectStack Data Adapter for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@objectstack/client": "^17.0.0-rc.
|
|
26
|
-
"@
|
|
27
|
-
"@object-ui/
|
|
25
|
+
"@objectstack/client": "^17.0.0-rc.2",
|
|
26
|
+
"@objectstack/spec": "^17.0.0-rc.2",
|
|
27
|
+
"@object-ui/core": "17.3.0",
|
|
28
|
+
"@object-ui/types": "17.3.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@objectstack/spec": "^17.0.0-rc.0",
|
|
31
31
|
"tsup": "^8.5.1",
|
|
32
32
|
"typescript": "^6.0.3",
|
|
33
33
|
"vitest": "^4.1.10"
|
|
@@ -17,9 +17,18 @@ interface CachedSchema {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Statistics reported by {@link MetadataCache.getStats} for monitoring.
|
|
21
|
+
*
|
|
22
|
+
* NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
|
|
23
|
+
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
|
|
24
|
+
* describes the platform's `ICacheService` — a server-side KV cache measured by
|
|
25
|
+
* `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
|
|
26
|
+
* of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
|
|
27
|
+
* concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
|
|
28
|
+
* Neither type has a key the other has, so this is a name collision, not a
|
|
29
|
+
* dialect — deriving would have replaced every field.
|
|
21
30
|
*/
|
|
22
|
-
export interface
|
|
31
|
+
export interface MetadataCacheStats {
|
|
23
32
|
size: number;
|
|
24
33
|
maxSize: number;
|
|
25
34
|
hits: number;
|
|
@@ -218,7 +227,7 @@ export class MetadataCache {
|
|
|
218
227
|
*
|
|
219
228
|
* @returns Cache statistics including hit rate
|
|
220
229
|
*/
|
|
221
|
-
getStats():
|
|
230
|
+
getStats(): MetadataCacheStats {
|
|
222
231
|
const total = this.stats.hits + this.stats.misses;
|
|
223
232
|
const hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
224
233
|
|
package/src/errors.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
BulkOperationError,
|
|
14
14
|
ConnectionError,
|
|
15
15
|
AuthenticationError,
|
|
16
|
-
|
|
16
|
+
DataApiValidationError,
|
|
17
17
|
createErrorFromResponse,
|
|
18
18
|
isObjectStackError,
|
|
19
19
|
isErrorType,
|
|
@@ -189,20 +189,20 @@ describe('Error Classes', () => {
|
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
describe('
|
|
192
|
+
describe('DataApiValidationError', () => {
|
|
193
193
|
it('should create validation error', () => {
|
|
194
|
-
const error = new
|
|
194
|
+
const error = new DataApiValidationError('Invalid input');
|
|
195
195
|
|
|
196
196
|
expect(error.message).toBe('Invalid input');
|
|
197
197
|
expect(error.code).toBe('VALIDATION_ERROR');
|
|
198
198
|
expect(error.statusCode).toBe(400);
|
|
199
199
|
expect(error.name).toBe('ValidationError');
|
|
200
200
|
expect(error).toBeInstanceOf(ObjectStackError);
|
|
201
|
-
expect(error).toBeInstanceOf(
|
|
201
|
+
expect(error).toBeInstanceOf(DataApiValidationError);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
it('should include field information', () => {
|
|
205
|
-
const error = new
|
|
205
|
+
const error = new DataApiValidationError('Email is invalid', 'email');
|
|
206
206
|
|
|
207
207
|
expect(error.field).toBe('email');
|
|
208
208
|
expect(error.details).toHaveProperty('field', 'email');
|
|
@@ -214,7 +214,7 @@ describe('Error Classes', () => {
|
|
|
214
214
|
{ field: 'age', message: 'Must be a positive number' },
|
|
215
215
|
];
|
|
216
216
|
|
|
217
|
-
const error = new
|
|
217
|
+
const error = new DataApiValidationError(
|
|
218
218
|
'Validation failed',
|
|
219
219
|
undefined,
|
|
220
220
|
validationErrors
|
|
@@ -225,7 +225,7 @@ describe('Error Classes', () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it('should return empty array when no validation errors', () => {
|
|
228
|
-
const error = new
|
|
228
|
+
const error = new DataApiValidationError('Validation failed');
|
|
229
229
|
|
|
230
230
|
expect(error.getValidationErrors()).toEqual([]);
|
|
231
231
|
});
|
|
@@ -286,7 +286,7 @@ describe('Error Helpers', () => {
|
|
|
286
286
|
expect(error.code).toBe('NOT_FOUND');
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
it('should create
|
|
289
|
+
it('should create DataApiValidationError for 400 status', () => {
|
|
290
290
|
const response = {
|
|
291
291
|
status: 400,
|
|
292
292
|
message: 'Bad request',
|
|
@@ -299,9 +299,9 @@ describe('Error Helpers', () => {
|
|
|
299
299
|
|
|
300
300
|
const error = createErrorFromResponse(response);
|
|
301
301
|
|
|
302
|
-
expect(error).toBeInstanceOf(
|
|
302
|
+
expect(error).toBeInstanceOf(DataApiValidationError);
|
|
303
303
|
expect(error.statusCode).toBe(400);
|
|
304
|
-
expect((error as
|
|
304
|
+
expect((error as DataApiValidationError).validationErrors).toEqual([
|
|
305
305
|
{ field: 'email', message: 'Invalid email' },
|
|
306
306
|
]);
|
|
307
307
|
});
|
|
@@ -379,7 +379,7 @@ describe('Error Helpers', () => {
|
|
|
379
379
|
const bulkError = new BulkOperationError('create', 0, 1, []);
|
|
380
380
|
const connError = new ConnectionError('timeout');
|
|
381
381
|
const authError = new AuthenticationError();
|
|
382
|
-
const validError = new
|
|
382
|
+
const validError = new DataApiValidationError('invalid');
|
|
383
383
|
|
|
384
384
|
expect(isObjectStackError(metadataError)).toBe(true);
|
|
385
385
|
expect(isObjectStackError(bulkError)).toBe(true);
|
|
@@ -409,7 +409,7 @@ describe('Error Helpers', () => {
|
|
|
409
409
|
|
|
410
410
|
it('should return false for non-matching error type', () => {
|
|
411
411
|
const error = new MetadataNotFoundError('users');
|
|
412
|
-
expect(isErrorType(error,
|
|
412
|
+
expect(isErrorType(error, DataApiValidationError)).toBe(false);
|
|
413
413
|
});
|
|
414
414
|
|
|
415
415
|
it('should return true for base class check', () => {
|
package/src/errors.ts
CHANGED
|
@@ -159,12 +159,24 @@ export class AuthenticationError extends ObjectStackError {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Error thrown when data
|
|
162
|
+
* Error thrown when the ObjectStack data API rejects a write as invalid.
|
|
163
|
+
*
|
|
164
|
+
* NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
|
|
165
|
+
* class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
|
|
166
|
+
* a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
|
|
167
|
+
* manifest's validation report — and `@object-ui/types` re-exports it under that
|
|
168
|
+
* name. This is a runtime `Error` subclass carrying an HTTP status plus a list
|
|
169
|
+
* of such entries, so the two are not even the same KIND of thing.
|
|
170
|
+
*
|
|
171
|
+
* The name follows the convention registered on objectstack#4115 for this
|
|
172
|
+
* family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
|
|
173
|
+
* took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
|
|
174
|
+
* the data API.
|
|
163
175
|
*/
|
|
164
|
-
export class
|
|
176
|
+
export class DataApiValidationError extends ObjectStackError {
|
|
165
177
|
/**
|
|
166
|
-
* Create a new
|
|
167
|
-
*
|
|
178
|
+
* Create a new DataApiValidationError
|
|
179
|
+
*
|
|
168
180
|
* @param message - Human-readable error message
|
|
169
181
|
* @param field - The field that failed validation (optional)
|
|
170
182
|
* @param validationErrors - Array of validation error details
|
|
@@ -186,6 +198,12 @@ export class ValidationError extends ObjectStackError {
|
|
|
186
198
|
...details,
|
|
187
199
|
}
|
|
188
200
|
);
|
|
201
|
+
// The RUNTIME name stays `'ValidationError'` on purpose — it is the wire
|
|
202
|
+
// discriminator this adapter shares with `@objectstack/client` and with
|
|
203
|
+
// consumers that sniff `err.name` rather than `instanceof`
|
|
204
|
+
// (`normaliseClientError` here, `@object-ui/react`'s `error-message`).
|
|
205
|
+
// Renaming the TypeScript symbol is a source-level rename; renaming this
|
|
206
|
+
// string would be a behaviour change nobody asked for.
|
|
189
207
|
this.name = 'ValidationError';
|
|
190
208
|
}
|
|
191
209
|
|
|
@@ -232,7 +250,7 @@ export function createErrorFromResponse(response: Record<string, unknown>, conte
|
|
|
232
250
|
return new ObjectStackError(message, 'NOT_FOUND', 404, details);
|
|
233
251
|
|
|
234
252
|
case 400:
|
|
235
|
-
return new
|
|
253
|
+
return new DataApiValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);
|
|
236
254
|
|
|
237
255
|
case 503:
|
|
238
256
|
return new ConnectionError(message, (response?.config as Record<string, unknown>)?.url as string, details, 503);
|