@object-ui/data-objectstack 17.1.0 → 17.2.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 +125 -0
- package/README.md +6 -1
- package/dist/index.cjs +7 -7
- package/dist/index.d.cts +66 -28
- package/dist/index.d.ts +66 -28
- package/dist/index.js +6 -6
- 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 +22 -16
- package/src/metadata-client.ts +17 -3
- package/src/security.ts +18 -5
- package/src/spec-symbol-batch6.test.ts +303 -0
- package/src/validation-error.test.ts +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.2.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.1",
|
|
26
|
+
"@objectstack/spec": "^17.0.0-rc.1",
|
|
27
|
+
"@object-ui/core": "17.2.0",
|
|
28
|
+
"@object-ui/types": "17.2.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);
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
|
|
10
|
+
import type { DroppedFieldsEvent } from '@objectstack/spec/data';
|
|
10
11
|
import type {
|
|
11
12
|
DataSource,
|
|
12
13
|
BatchTransactionOperation,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
MetadataNotFoundError,
|
|
40
41
|
BulkOperationError,
|
|
41
42
|
ConnectionError,
|
|
42
|
-
|
|
43
|
+
DataApiValidationError,
|
|
43
44
|
createErrorFromResponse,
|
|
44
45
|
} from './errors';
|
|
45
46
|
|
|
@@ -588,7 +589,7 @@ export function isConcurrentUpdateError(error: unknown): error is ConcurrentUpda
|
|
|
588
589
|
*
|
|
589
590
|
* Two shapes are recognised:
|
|
590
591
|
* - `409` + `CONCURRENT_UPDATE` → {@link ConcurrentUpdateError};
|
|
591
|
-
* - `400` + `VALIDATION_FAILED` → {@link
|
|
592
|
+
* - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
|
|
592
593
|
* server's per-field entries so a form can mark the offending inputs
|
|
593
594
|
* instead of showing one undirected toast.
|
|
594
595
|
*/
|
|
@@ -621,7 +622,7 @@ export function normaliseClientError(error: unknown): unknown {
|
|
|
621
622
|
})
|
|
622
623
|
.filter((x): x is { field: string; message: string } => x !== null);
|
|
623
624
|
|
|
624
|
-
return new
|
|
625
|
+
return new DataApiValidationError(
|
|
625
626
|
typeof e.message === 'string' ? e.message : 'Validation failed',
|
|
626
627
|
validationErrors[0]?.field,
|
|
627
628
|
validationErrors,
|
|
@@ -721,16 +722,21 @@ export type BatchProgressListener = (event: BatchProgressEvent) => void;
|
|
|
721
722
|
/**
|
|
722
723
|
* One server-reported write-strip: caller-supplied fields the backend LEGALLY
|
|
723
724
|
* removed from a write before persisting (a non-system caller cannot seed a
|
|
724
|
-
* `readonly` field, a `readonlyWhen` predicate locked it, etc.).
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
725
|
+
* `readonly` field, a `readonlyWhen` predicate locked it, etc.).
|
|
726
|
+
*
|
|
727
|
+
* THE spec type, re-exported (objectui#3160, objectstack#4115 ledger batch 6).
|
|
728
|
+
* Until then this was a hand copy whose comment said it "mirrors the framework
|
|
729
|
+
* `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally so we
|
|
730
|
+
* don't pin a client type version", with `reason` widened from the spec's
|
|
731
|
+
* `'readonly' | 'readonly_when'` to bare `string` "for forward-compatibility
|
|
732
|
+
* with reasons added server-side". Both halves of that reasoning are the
|
|
733
|
+
* failure mode this ledger exists to remove: the spec IS the client type
|
|
734
|
+
* version, and a consumer-side widening of a producer's enum is precisely the
|
|
735
|
+
* lenient fallback AGENTS.md #12 bans — it deletes the only compile-time signal
|
|
736
|
+
* that would tell `AdapterProvider`'s toast wording (which branches on
|
|
737
|
+
* `readonly_when`) that a new reason had appeared.
|
|
728
738
|
*/
|
|
729
|
-
export
|
|
730
|
-
object: string;
|
|
731
|
-
fields: string[];
|
|
732
|
-
reason: string;
|
|
733
|
-
}
|
|
739
|
+
export type { DroppedFieldsEvent };
|
|
734
740
|
|
|
735
741
|
/**
|
|
736
742
|
* Emitted after a create/update whose response carried `droppedFields`
|
|
@@ -3511,14 +3517,14 @@ export {
|
|
|
3511
3517
|
BulkOperationError,
|
|
3512
3518
|
ConnectionError,
|
|
3513
3519
|
AuthenticationError,
|
|
3514
|
-
|
|
3520
|
+
DataApiValidationError,
|
|
3515
3521
|
createErrorFromResponse,
|
|
3516
3522
|
isObjectStackError,
|
|
3517
3523
|
isErrorType,
|
|
3518
3524
|
} from './errors';
|
|
3519
3525
|
|
|
3520
3526
|
// Export cache types
|
|
3521
|
-
export type {
|
|
3527
|
+
export type { MetadataCacheStats } from './cache/MetadataCache';
|
|
3522
3528
|
|
|
3523
3529
|
// v3.0.0 Deep Integration modules
|
|
3524
3530
|
export { CloudOperations } from './cloud';
|
|
@@ -3546,7 +3552,7 @@ export type {
|
|
|
3546
3552
|
MetadataClientConfig,
|
|
3547
3553
|
MetadataListOptions,
|
|
3548
3554
|
MetadataDraftHeader,
|
|
3549
|
-
|
|
3555
|
+
MetadataClientSaveOptions,
|
|
3550
3556
|
MetadataGetOptions,
|
|
3551
3557
|
MetadataDeleteOptions,
|
|
3552
3558
|
MetadataHistoryOptions,
|
|
@@ -3563,7 +3569,7 @@ export type {
|
|
|
3563
3569
|
} from './metadata-client';
|
|
3564
3570
|
|
|
3565
3571
|
export { SecurityManager } from './security';
|
|
3566
|
-
export type {
|
|
3572
|
+
export type { SecurityManagerPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
|
|
3567
3573
|
|
|
3568
3574
|
export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
|
|
3569
3575
|
export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';
|
package/src/metadata-client.ts
CHANGED
|
@@ -75,7 +75,21 @@ export interface MetadataDraftHeader {
|
|
|
75
75
|
updatedBy: string | null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
|
|
80
|
+
* `/api/v1/meta/:type/:name`.
|
|
81
|
+
*
|
|
82
|
+
* NOT the spec's `MetadataSaveOptions` (`@objectstack/spec/system` and
|
|
83
|
+
* `/kernel`), whose name this interface wore until objectui#3160
|
|
84
|
+
* (objectstack#4115 ledger batch 6). Both spec copies describe writing a
|
|
85
|
+
* metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
|
|
86
|
+
* `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
|
|
87
|
+
* exists here, and not one of these exists there: this is the REST client's
|
|
88
|
+
* request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
|
|
89
|
+
* attribution, the destructive-change override, the ADR-0033 draft/publish
|
|
90
|
+
* mode, and the owning package. Same words, different layer.
|
|
91
|
+
*/
|
|
92
|
+
export interface MetadataClientSaveOptions {
|
|
79
93
|
/**
|
|
80
94
|
* Optimistic concurrency token (the `checksum` returned by the last
|
|
81
95
|
* read). When present, sent as the `If-Match` header so concurrent
|
|
@@ -122,7 +136,7 @@ export interface MetadataGetOptions {
|
|
|
122
136
|
packageId?: string;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
export interface MetadataDeleteOptions extends
|
|
139
|
+
export interface MetadataDeleteOptions extends MetadataClientSaveOptions {
|
|
126
140
|
/**
|
|
127
141
|
* Target state. `'draft'` discards the pending draft (keeps the
|
|
128
142
|
* published overlay intact). Omit to reset the active overlay back
|
|
@@ -527,7 +541,7 @@ export class MetadataClient {
|
|
|
527
541
|
type: string,
|
|
528
542
|
name: string,
|
|
529
543
|
item: unknown,
|
|
530
|
-
options:
|
|
544
|
+
options: MetadataClientSaveOptions = {},
|
|
531
545
|
): Promise<T> {
|
|
532
546
|
if (!name || !String(name).trim()) {
|
|
533
547
|
// The `PUT /meta/:type/:name` route rejects a missing `:name` segment
|
package/src/security.ts
CHANGED
|
@@ -11,7 +11,20 @@
|
|
|
11
11
|
* Provides advanced security policies: CSP config, audit logging, data masking.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for {@link SecurityManager} — this adapter's browser-side
|
|
16
|
+
* security posture: the CSP header it generates, the in-memory audit log it
|
|
17
|
+
* keeps, and the field-masking rules it applies before a record is rendered.
|
|
18
|
+
*
|
|
19
|
+
* NOT the spec's `SecurityPolicy` (`@objectstack/spec/kernel`), whose name this
|
|
20
|
+
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
|
|
21
|
+
* is the PACKAGE SUPPLY-CHAIN policy — `{ id, name, autoScan, thresholds,
|
|
22
|
+
* allowedLicenses, prohibitedLicenses, codeSigning, sandbox }` — the rules a
|
|
23
|
+
* marketplace applies when scanning a plugin before install. It shares no key
|
|
24
|
+
* with this one, so there was nothing to derive; the collision is the word
|
|
25
|
+
* "security", not the concept.
|
|
26
|
+
*/
|
|
27
|
+
export interface SecurityManagerPolicy {
|
|
15
28
|
/** Content Security Policy configuration */
|
|
16
29
|
csp?: CSPConfig;
|
|
17
30
|
/** Audit logging configuration */
|
|
@@ -100,10 +113,10 @@ export interface AuditLogEntry {
|
|
|
100
113
|
* Handles CSP generation, audit logging, and data masking.
|
|
101
114
|
*/
|
|
102
115
|
export class SecurityManager {
|
|
103
|
-
private policy:
|
|
116
|
+
private policy: SecurityManagerPolicy;
|
|
104
117
|
private auditLog: AuditLogEntry[] = [];
|
|
105
118
|
|
|
106
|
-
constructor(policy:
|
|
119
|
+
constructor(policy: SecurityManagerPolicy = {}) {
|
|
107
120
|
this.policy = policy;
|
|
108
121
|
}
|
|
109
122
|
|
|
@@ -204,14 +217,14 @@ export class SecurityManager {
|
|
|
204
217
|
/**
|
|
205
218
|
* Update the security policy.
|
|
206
219
|
*/
|
|
207
|
-
updatePolicy(policy: Partial<
|
|
220
|
+
updatePolicy(policy: Partial<SecurityManagerPolicy>): void {
|
|
208
221
|
this.policy = { ...this.policy, ...policy };
|
|
209
222
|
}
|
|
210
223
|
|
|
211
224
|
/**
|
|
212
225
|
* Get current security policy.
|
|
213
226
|
*/
|
|
214
|
-
getPolicy():
|
|
227
|
+
getPolicy(): SecurityManagerPolicy {
|
|
215
228
|
return { ...this.policy };
|
|
216
229
|
}
|
|
217
230
|
}
|