@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "17.1.0",
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.0",
26
- "@object-ui/core": "17.1.0",
27
- "@object-ui/types": "17.1.0"
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
- * Cache statistics for monitoring
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 CacheStats {
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(): CacheStats {
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
 
@@ -13,7 +13,7 @@ import {
13
13
  BulkOperationError,
14
14
  ConnectionError,
15
15
  AuthenticationError,
16
- ValidationError,
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('ValidationError', () => {
192
+ describe('DataApiValidationError', () => {
193
193
  it('should create validation error', () => {
194
- const error = new ValidationError('Invalid input');
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(ValidationError);
201
+ expect(error).toBeInstanceOf(DataApiValidationError);
202
202
  });
203
203
 
204
204
  it('should include field information', () => {
205
- const error = new ValidationError('Email is invalid', 'email');
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 ValidationError(
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 ValidationError('Validation failed');
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 ValidationError for 400 status', () => {
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(ValidationError);
302
+ expect(error).toBeInstanceOf(DataApiValidationError);
303
303
  expect(error.statusCode).toBe(400);
304
- expect((error as ValidationError).validationErrors).toEqual([
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 ValidationError('invalid');
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, ValidationError)).toBe(false);
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 validation fails
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 ValidationError extends ObjectStackError {
176
+ export class DataApiValidationError extends ObjectStackError {
165
177
  /**
166
- * Create a new ValidationError
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 ValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);
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
- ValidationError,
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 ValidationError}, carrying the
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 ValidationError(
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.). Mirrors the
725
- * framework `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally
726
- * so we don't pin a client type version — `reason` is kept as a widened string
727
- * for forward-compatibility with reasons added server-side.
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 interface DroppedFieldsEvent {
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
- ValidationError,
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 { CacheStats } from './cache/MetadataCache';
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
- MetadataSaveOptions,
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 { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
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';
@@ -75,7 +75,21 @@ export interface MetadataDraftHeader {
75
75
  updatedBy: string | null;
76
76
  }
77
77
 
78
- export interface MetadataSaveOptions {
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 MetadataSaveOptions {
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: MetadataSaveOptions = {},
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
- export interface SecurityPolicy {
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: SecurityPolicy;
116
+ private policy: SecurityManagerPolicy;
104
117
  private auditLog: AuditLogEntry[] = [];
105
118
 
106
- constructor(policy: SecurityPolicy = {}) {
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<SecurityPolicy>): void {
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(): SecurityPolicy {
227
+ getPolicy(): SecurityManagerPolicy {
215
228
  return { ...this.policy };
216
229
  }
217
230
  }