@object-ui/data-objectstack 17.5.0 → 17.7.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +2311 -0
  2. package/README.md +557 -11
  3. package/dist/index.cjs +1663 -463
  4. package/dist/index.d.cts +1780 -518
  5. package/dist/index.d.ts +1780 -518
  6. package/dist/index.js +1648 -457
  7. package/package.json +9 -9
  8. package/dist/tsconfig.tsbuildinfo +0 -1
  9. package/src/aggregate-capability.test.ts +0 -177
  10. package/src/apiAccessDenied.test.ts +0 -233
  11. package/src/appAccessProbe.test.ts +0 -147
  12. package/src/bulkDelete.test.ts +0 -75
  13. package/src/bulkUpdate.test.ts +0 -93
  14. package/src/cache/MetadataCache.test.ts +0 -477
  15. package/src/cache/MetadataCache.ts +0 -289
  16. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  17. package/src/connection.test.ts +0 -188
  18. package/src/contracts.ts +0 -115
  19. package/src/deleteView.homes.test.ts +0 -389
  20. package/src/deleteViewContract.types.test.ts +0 -246
  21. package/src/errors.test.ts +0 -426
  22. package/src/errors.ts +0 -293
  23. package/src/exportDownload.test.ts +0 -141
  24. package/src/filter-entry-translation.test.ts +0 -360
  25. package/src/filter-operator-ast-parity.test.ts +0 -135
  26. package/src/getObjectSchema.test.ts +0 -145
  27. package/src/httpFailureLogging.test.ts +0 -109
  28. package/src/importJob.test.ts +0 -71
  29. package/src/index.ts +0 -4442
  30. package/src/integration.ts +0 -200
  31. package/src/listViewOverrides.test.ts +0 -241
  32. package/src/listViews.test.ts +0 -175
  33. package/src/metadata-client.get-envelope.test.ts +0 -171
  34. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  35. package/src/metadata-client.test.ts +0 -303
  36. package/src/metadata-client.ts +0 -1046
  37. package/src/occ.test.ts +0 -101
  38. package/src/onMutation.test.ts +0 -589
  39. package/src/onSaveAdvisory.test.ts +0 -311
  40. package/src/onWriteWarning.test.ts +0 -273
  41. package/src/orderby-serialization.test.ts +0 -115
  42. package/src/queryDataset.test.ts +0 -543
  43. package/src/searchAll.test.ts +0 -120
  44. package/src/security.ts +0 -243
  45. package/src/spec-symbol-batch6.test.ts +0 -326
  46. package/src/studio.ts +0 -152
  47. package/src/updateView.draft.test.ts +0 -242
  48. package/src/userState.test.ts +0 -315
  49. package/src/userState.ts +0 -238
  50. package/src/v3-compat.test.ts +0 -229
  51. package/src/validation-error.test.ts +0 -94
  52. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  53. package/src/viewInvalidationSeam.pin.test.ts +0 -162
@@ -1,246 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi } from 'vitest';
10
- import type { DataSource, DeleteViewResult, ViewHomeDeleteOutcome } from '@object-ui/types';
11
- import {
12
- ObjectStackAdapter,
13
- type DeleteViewResult as AdapterDeleteViewResult,
14
- type ViewHomeDeleteOutcome as AdapterViewHomeDeleteOutcome,
15
- } from './index';
16
-
17
- /**
18
- * The shared `DataSource` contract carries `deleteView`'s per-home outcomes
19
- * (#4564).
20
- *
21
- * #4479 / PR #4562 widened THIS adapter's `deleteView` to return
22
- * `DeleteViewResult { deleted, draft?, published? }`, but the shared interface
23
- * in `@object-ui/types` still declared the narrow `{ deleted: boolean }`. The
24
- * adapter's wider return is assignable to the narrower declaration, so nothing
25
- * failed to compile — which is exactly what made the gap invisible: a consumer
26
- * reaching the adapter THROUGH `DataSource` was handed a type that had already
27
- * thrown the per-home outcomes away. The one real call site today (app-shell's
28
- * `ObjectView` delete handler) reads nothing off the receipt, so the loss was
29
- * latent rather than broken.
30
- *
31
- * #4564's ruling moved the canonical shapes DOWN to `@object-ui/types` beside
32
- * the interface (the dependency runs data-objectstack -> types, so the shapes
33
- * could not be imported upward), widened `DataSource.deleteView?`'s declared
34
- * return to `DeleteViewResult`, and left this package re-exporting both names
35
- * so every existing importer keeps compiling.
36
- *
37
- * ## Where these pins get their colour
38
- *
39
- * Most of this file is COMPILE-time. That is not a weaker pin, it is the only
40
- * pin that can observe this defect: the bug was never a wrong value at runtime,
41
- * it was a declaration that could not describe the value. `vitest` transpiles
42
- * with esbuild and erases types, so the runtime case below passed just as
43
- * happily against the narrow declaration — its `receipt.draft` read was a
44
- * `tsc` error and a green test at the same time. The colour therefore comes
45
- * from `pnpm --filter @object-ui/data-objectstack type-check`, and this
46
- * package's `tsconfig.json` includes its whole `src/**` (tests included), so
47
- * these assertions are checked there and by CI's Type Check job with no
48
- * separate `tsconfig.typetests.json` — the same property `queryDataset.test.ts`
49
- * and `spec-symbol-batch6.test.ts` document and rely on.
50
- *
51
- * ## The discrimination control
52
- *
53
- * `NarrowLegacyResult` below is `deleteView`'s declared return BEFORE #4564,
54
- * kept as a live control rather than described in prose. Every "the contract
55
- * carries the per-home outcomes" assertion is paired with the control failing
56
- * to carry them. Undo the move and the paired assertions go red together,
57
- * which is what stops this file from degrading into a restatement of whatever
58
- * the interface happens to say.
59
- */
60
-
61
- const VIEW = 'account.my_pipeline';
62
-
63
- /** Framework receipt: a draft row existed and was discarded. */
64
- const DRAFT_DISCARDED = {
65
- success: true,
66
- reset: true,
67
- seq: 41,
68
- message: `Draft discarded — view/${VIEW}. [seq=41]`,
69
- };
70
-
71
- /** Framework receipt: no published row. A 200 carrying `reset:false`, not a 404. */
72
- const NO_PUBLISHED = {
73
- success: true,
74
- reset: false,
75
- message: `No view '${VIEW}' found — nothing to delete.`,
76
- };
77
-
78
- /**
79
- * The smallest adapter that can answer both view-delete addresses.
80
- *
81
- * Deliberately NOT a second copy of `deleteView.homes.test.ts`'s harness: that
82
- * file owns the behaviour (15 pins over both homes, the ordering, the failure
83
- * paths) and stays untouched. This one exists only to drive one call through a
84
- * `DataSource`-typed handle, so the per-home read below is served by the shared
85
- * declaration and by nothing else.
86
- */
87
- function makeAdapter(): ObjectStackAdapter {
88
- const baseUrl = 'http://test.local';
89
- const json = (body: unknown) =>
90
- new Response(JSON.stringify(body), {
91
- status: 200,
92
- headers: { 'Content-Type': 'application/json' },
93
- });
94
-
95
- const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
96
- const url = String(input);
97
- if (url.includes('/meta/view/') && (init?.method ?? 'GET') === 'DELETE') {
98
- return json(url.includes('state=draft') ? DRAFT_DISCARDED : NO_PUBLISHED);
99
- }
100
- return json({ success: true, data: { capabilities: {}, routes: {} } });
101
- });
102
-
103
- const adapter = new ObjectStackAdapter({ baseUrl, fetch: fetchImpl });
104
- const poke = adapter as any;
105
- poke.connected = true;
106
- poke.connectionState = 'connected';
107
- poke.metadataCache = {
108
- get: async (_key: string, loader: () => Promise<any>) => loader(),
109
- invalidate: () => undefined,
110
- getCachedSync: () => undefined,
111
- getStats: () => ({}),
112
- };
113
- return adapter;
114
- }
115
-
116
- /* -------------------------------------------------------------------------- */
117
- /* Compile-time vocabulary — the repo's idiom (queryDataset.test.ts). */
118
- /* -------------------------------------------------------------------------- */
119
-
120
- type Assert<T extends true> = T;
121
- type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
122
- ? true
123
- : false;
124
- type HasKey<T, K extends PropertyKey> = K extends keyof T ? true : false;
125
-
126
- /** The shared contract's declared return, read off the interface itself. */
127
- type ContractDeleteView = NonNullable<DataSource['deleteView']>;
128
- type ContractReturn = Awaited<ReturnType<ContractDeleteView>>;
129
-
130
- /**
131
- * `DataSource.deleteView`'s declared return BEFORE #4564 — the control.
132
- * Kept as a type, not a comment, so "the contract used to lose the per-home
133
- * outcomes" is an assertion the compiler re-checks on every run.
134
- */
135
- interface NarrowLegacyResult {
136
- deleted: boolean;
137
- }
138
-
139
- describe('DataSource.deleteView declares the per-home outcomes (#4564)', () => {
140
- it('the shared contract returns the canonical DeleteViewResult, not a restatement', () => {
141
- // `Equal` rather than a two-way `extends`: a structurally overlapping
142
- // re-declaration in `types` would satisfy mutual assignability while
143
- // drifting, and a hand copy is precisely what the one-resolver rule and
144
- // this card's ruling rejected in favour of moving the declaration.
145
- type _IsTheCanonicalShape = Assert<Equal<ContractReturn, DeleteViewResult>>;
146
-
147
- expect(true).toBe(true);
148
- });
149
-
150
- it('carries `draft` and `published`, where the pre-#4564 declaration carried neither', () => {
151
- type _CarriesDraft = Assert<Equal<HasKey<ContractReturn, 'draft'>, true>>;
152
- type _CarriesPublished = Assert<Equal<HasKey<ContractReturn, 'published'>, true>>;
153
-
154
- // The control, and the whole reason this file is a pin rather than an echo:
155
- // the shape the interface used to declare cannot answer either read. If the
156
- // move is reverted, the two assertions above go red and these two stay
157
- // green — the difference between them IS #4564's delta.
158
- type _ControlHasNoDraft = Assert<Equal<HasKey<NarrowLegacyResult, 'draft'>, false>>;
159
- type _ControlHasNoPublished = Assert<Equal<HasKey<NarrowLegacyResult, 'published'>, false>>;
160
- type _ContractIsNotTheNarrowShape = Assert<
161
- Equal<Equal<ContractReturn, NarrowLegacyResult>, false>
162
- >;
163
-
164
- expect(true).toBe(true);
165
- });
166
-
167
- it('types the per-home outcomes as ViewHomeDeleteOutcome, optional on both homes', () => {
168
- type _DraftIsTheOutcome = Assert<Equal<ContractReturn['draft'], ViewHomeDeleteOutcome | undefined>>;
169
- type _PublishedIsTheOutcome = Assert<
170
- Equal<ContractReturn['published'], ViewHomeDeleteOutcome | undefined>
171
- >;
172
- // `deleted` is unchanged — the growth is additive, so a consumer reading
173
- // only `deleted` is untouched by the widening.
174
- type _DeletedUnchanged = Assert<Equal<ContractReturn['deleted'], boolean>>;
175
-
176
- expect(true).toBe(true);
177
- });
178
-
179
- it('MUST NOT CHANGE: deleteView stays OPTIONAL, with the same two parameters', () => {
180
- // The widening is to the RETURN only. An adapter that never implemented
181
- // `deleteView` must stay a legal `DataSource`.
182
- type _StillOptional = Assert<undefined extends DataSource['deleteView'] ? true : false>;
183
- type _SameParameters = Assert<Equal<Parameters<ContractDeleteView>, [string, string]>>;
184
-
185
- expect(true).toBe(true);
186
- });
187
- });
188
-
189
- describe('the data-objectstack spellings survive the move (#4564)', () => {
190
- it('re-exports both names as the SAME declaration, not a structural twin', () => {
191
- // The re-export contract: every importer that PR #4562 left pointing at
192
- // `@object-ui/data-objectstack` keeps compiling, and keeps getting the type
193
- // the shared interface now speaks. `Equal` is the assertion that matters —
194
- // two identical-looking declarations are mutually assignable, so only
195
- // identity can tell a re-export from a copy that will drift.
196
- type _ResultIsTheTypesSpelling = Assert<Equal<AdapterDeleteViewResult, DeleteViewResult>>;
197
- type _OutcomeIsTheTypesSpelling = Assert<
198
- Equal<AdapterViewHomeDeleteOutcome, ViewHomeDeleteOutcome>
199
- >;
200
-
201
- expect(true).toBe(true);
202
- });
203
-
204
- it('the adapter method still satisfies the widened contract', () => {
205
- const adapter = makeAdapter();
206
-
207
- // Explicit, because `class ObjectStackAdapter implements DataSource` would
208
- // have gone on compiling with the NARROW declaration too — a wider return
209
- // is assignable to a narrower one, which is how the gap survived #4562's
210
- // review in the first place. This states the direction the card cares
211
- // about instead of inheriting it from the class heritage clause.
212
- const deleteView = adapter.deleteView.bind(adapter) satisfies ContractDeleteView;
213
- expect(typeof deleteView).toBe('function');
214
-
215
- type _AdapterAssignableToContract = Assert<
216
- ObjectStackAdapter['deleteView'] extends ContractDeleteView ? true : false
217
- >;
218
- // ...and no longer merely assignable: the two returns are now one type, so
219
- // the adapter cannot widen further without the contract following it.
220
- type _ReturnsAreOneType = Assert<
221
- Equal<Awaited<ReturnType<ObjectStackAdapter['deleteView']>>, ContractReturn>
222
- >;
223
-
224
- expect(true).toBe(true);
225
- });
226
- });
227
-
228
- describe('a consumer holding only the shared DataSource (#4564)', () => {
229
- it('reads the per-home outcomes the adapter actually returns', async () => {
230
- const adapter = makeAdapter();
231
-
232
- // The narrowing that makes this a #4564 pin rather than a duplicate of
233
- // `deleteView.homes.test.ts`: the receipt below is typed by `DataSource`,
234
- // not by `ObjectStackAdapter`. Before the move this assignment compiled
235
- // just as well and threw the per-home outcomes away on the way through.
236
- const ds: DataSource = adapter;
237
-
238
- const receipt = await ds.deleteView!('account', VIEW);
239
-
240
- // Draft-only view: the draft home held the row, the published one did not.
241
- expect(receipt.deleted).toBe(true);
242
- expect(receipt.draft?.removed).toBe(true);
243
- expect(receipt.published?.removed).toBe(false);
244
- expect(receipt.draft?.message).toContain('Draft discarded');
245
- });
246
- });
@@ -1,426 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest';
10
- import {
11
- ObjectStackError,
12
- MetadataNotFoundError,
13
- BulkOperationError,
14
- ConnectionError,
15
- AuthenticationError,
16
- DataApiValidationError,
17
- createErrorFromResponse,
18
- isObjectStackError,
19
- isErrorType,
20
- } from './errors';
21
-
22
- describe('Error Classes', () => {
23
- describe('ObjectStackError', () => {
24
- it('should create base error with all properties', () => {
25
- const error = new ObjectStackError(
26
- 'Test error',
27
- 'TEST_ERROR',
28
- 500,
29
- { extra: 'info' }
30
- );
31
-
32
- expect(error.message).toBe('Test error');
33
- expect(error.code).toBe('TEST_ERROR');
34
- expect(error.statusCode).toBe(500);
35
- expect(error.details).toEqual({ extra: 'info' });
36
- expect(error.name).toBe('ObjectStackError');
37
- expect(error).toBeInstanceOf(Error);
38
- expect(error).toBeInstanceOf(ObjectStackError);
39
- });
40
-
41
- it('should work without optional parameters', () => {
42
- const error = new ObjectStackError('Test error', 'TEST_ERROR');
43
-
44
- expect(error.message).toBe('Test error');
45
- expect(error.code).toBe('TEST_ERROR');
46
- expect(error.statusCode).toBeUndefined();
47
- expect(error.details).toBeUndefined();
48
- });
49
-
50
- it('should convert to JSON correctly', () => {
51
- const error = new ObjectStackError(
52
- 'Test error',
53
- 'TEST_ERROR',
54
- 500,
55
- { extra: 'info' }
56
- );
57
-
58
- const json = error.toJSON();
59
- expect(json).toHaveProperty('name', 'ObjectStackError');
60
- expect(json).toHaveProperty('message', 'Test error');
61
- expect(json).toHaveProperty('code', 'TEST_ERROR');
62
- expect(json).toHaveProperty('statusCode', 500);
63
- expect(json).toHaveProperty('details', { extra: 'info' });
64
- expect(json).toHaveProperty('stack');
65
- });
66
-
67
- it('should maintain proper stack trace', () => {
68
- const error = new ObjectStackError('Test error', 'TEST_ERROR');
69
- expect(error.stack).toBeDefined();
70
- expect(error.stack).toContain('ObjectStackError');
71
- });
72
- });
73
-
74
- describe('MetadataNotFoundError', () => {
75
- it('should create metadata not found error', () => {
76
- const error = new MetadataNotFoundError('users');
77
-
78
- expect(error.message).toBe('Metadata not found for object: users');
79
- expect(error.code).toBe('METADATA_NOT_FOUND');
80
- expect(error.statusCode).toBe(404);
81
- expect(error.name).toBe('MetadataNotFoundError');
82
- expect(error.details).toHaveProperty('objectName', 'users');
83
- expect(error).toBeInstanceOf(ObjectStackError);
84
- expect(error).toBeInstanceOf(MetadataNotFoundError);
85
- });
86
-
87
- it('should include additional details', () => {
88
- const error = new MetadataNotFoundError('users', { reason: 'Schema not loaded' });
89
-
90
- expect(error.details).toEqual({
91
- objectName: 'users',
92
- reason: 'Schema not loaded',
93
- });
94
- });
95
- });
96
-
97
- describe('BulkOperationError', () => {
98
- it('should create bulk operation error', () => {
99
- const errors = [
100
- { index: 0, error: 'Invalid data' },
101
- { index: 2, error: 'Duplicate key' },
102
- ];
103
-
104
- const error = new BulkOperationError('create', 8, 2, errors);
105
-
106
- expect(error.message).toBe('Bulk create operation failed: 8 succeeded, 2 failed');
107
- expect(error.code).toBe('BULK_OPERATION_ERROR');
108
- expect(error.statusCode).toBe(500);
109
- expect(error.name).toBe('BulkOperationError');
110
- expect(error.successCount).toBe(8);
111
- expect(error.failureCount).toBe(2);
112
- expect(error.errors).toEqual(errors);
113
- expect(error).toBeInstanceOf(ObjectStackError);
114
- expect(error).toBeInstanceOf(BulkOperationError);
115
- });
116
-
117
- it('should provide operation summary', () => {
118
- const errors = [{ index: 0, error: 'Error' }];
119
- const error = new BulkOperationError('update', 9, 1, errors);
120
-
121
- const summary = error.getSummary();
122
- expect(summary).toEqual({
123
- operation: 'update',
124
- total: 10,
125
- successful: 9,
126
- failed: 1,
127
- failureRate: 0.1,
128
- errors: errors,
129
- });
130
- });
131
-
132
- it('should handle different operation types', () => {
133
- const createError = new BulkOperationError('create', 5, 0, []);
134
- const updateError = new BulkOperationError('update', 3, 2, []);
135
- const deleteError = new BulkOperationError('delete', 10, 1, []);
136
-
137
- expect(createError.message).toContain('create');
138
- expect(updateError.message).toContain('update');
139
- expect(deleteError.message).toContain('delete');
140
- });
141
- });
142
-
143
- describe('ConnectionError', () => {
144
- it('should create connection error', () => {
145
- const error = new ConnectionError(
146
- 'Network timeout',
147
- 'https://api.example.com'
148
- );
149
-
150
- expect(error.message).toBe('Connection error: Network timeout');
151
- expect(error.code).toBe('CONNECTION_ERROR');
152
- expect(error.statusCode).toBe(503);
153
- expect(error.name).toBe('ConnectionError');
154
- expect(error.url).toBe('https://api.example.com');
155
- expect(error).toBeInstanceOf(ObjectStackError);
156
- expect(error).toBeInstanceOf(ConnectionError);
157
- });
158
-
159
- it('should work without URL', () => {
160
- const error = new ConnectionError('Network timeout');
161
-
162
- expect(error.url).toBeUndefined();
163
- expect(error.message).toBe('Connection error: Network timeout');
164
- });
165
- });
166
-
167
- describe('AuthenticationError', () => {
168
- it('should create authentication error', () => {
169
- const error = new AuthenticationError();
170
-
171
- expect(error.message).toBe('Authentication failed');
172
- expect(error.code).toBe('AUTHENTICATION_ERROR');
173
- expect(error.statusCode).toBe(401);
174
- expect(error.name).toBe('AuthenticationError');
175
- expect(error).toBeInstanceOf(ObjectStackError);
176
- expect(error).toBeInstanceOf(AuthenticationError);
177
- });
178
-
179
- it('should accept custom message', () => {
180
- const error = new AuthenticationError('Invalid API token');
181
-
182
- expect(error.message).toBe('Invalid API token');
183
- });
184
-
185
- it('should include additional details', () => {
186
- const error = new AuthenticationError('Invalid token', { token: 'abc123' });
187
-
188
- expect(error.details).toEqual({ token: 'abc123' });
189
- });
190
- });
191
-
192
- describe('DataApiValidationError', () => {
193
- it('should create validation error', () => {
194
- const error = new DataApiValidationError('Invalid input');
195
-
196
- expect(error.message).toBe('Invalid input');
197
- expect(error.code).toBe('VALIDATION_ERROR');
198
- expect(error.statusCode).toBe(400);
199
- expect(error.name).toBe('ValidationError');
200
- expect(error).toBeInstanceOf(ObjectStackError);
201
- expect(error).toBeInstanceOf(DataApiValidationError);
202
- });
203
-
204
- it('should include field information', () => {
205
- const error = new DataApiValidationError('Email is invalid', 'email');
206
-
207
- expect(error.field).toBe('email');
208
- expect(error.details).toHaveProperty('field', 'email');
209
- });
210
-
211
- it('should include validation errors array', () => {
212
- const validationErrors = [
213
- { field: 'email', message: 'Invalid email format' },
214
- { field: 'age', message: 'Must be a positive number' },
215
- ];
216
-
217
- const error = new DataApiValidationError(
218
- 'Validation failed',
219
- undefined,
220
- validationErrors
221
- );
222
-
223
- expect(error.validationErrors).toEqual(validationErrors);
224
- expect(error.getValidationErrors()).toEqual(validationErrors);
225
- });
226
-
227
- it('should return empty array when no validation errors', () => {
228
- const error = new DataApiValidationError('Validation failed');
229
-
230
- expect(error.getValidationErrors()).toEqual([]);
231
- });
232
- });
233
- });
234
-
235
- describe('Error Helpers', () => {
236
- describe('createErrorFromResponse', () => {
237
- it('should create AuthenticationError for 401 status', () => {
238
- const response = {
239
- status: 401,
240
- message: 'Unauthorized',
241
- data: null,
242
- };
243
-
244
- const error = createErrorFromResponse(response, 'API request');
245
-
246
- expect(error).toBeInstanceOf(AuthenticationError);
247
- expect(error.message).toBe('Unauthorized');
248
- expect(error.statusCode).toBe(401);
249
- });
250
-
251
- it('should create AuthenticationError for 403 status', () => {
252
- const response = {
253
- status: 403,
254
- message: 'Forbidden',
255
- };
256
-
257
- const error = createErrorFromResponse(response);
258
-
259
- expect(error).toBeInstanceOf(AuthenticationError);
260
- expect(error.statusCode).toBe(403);
261
- });
262
-
263
- it('should create MetadataNotFoundError for 404 with metadata context', () => {
264
- const response = {
265
- status: 404,
266
- message: 'Not found',
267
- };
268
-
269
- const error = createErrorFromResponse(response, 'getObjectSchema(users)');
270
-
271
- expect(error).toBeInstanceOf(MetadataNotFoundError);
272
- expect(error.statusCode).toBe(404);
273
- expect((error as MetadataNotFoundError).details?.objectName).toBe('users');
274
- });
275
-
276
- it('should create generic error for 404 without metadata context', () => {
277
- const response = {
278
- status: 404,
279
- message: 'Not found',
280
- };
281
-
282
- const error = createErrorFromResponse(response, 'data request');
283
-
284
- expect(error).toBeInstanceOf(ObjectStackError);
285
- expect(error).not.toBeInstanceOf(MetadataNotFoundError);
286
- expect(error.code).toBe('NOT_FOUND');
287
- });
288
-
289
- it('should create DataApiValidationError for 400 status', () => {
290
- const response = {
291
- status: 400,
292
- message: 'Bad request',
293
- data: {
294
- errors: [
295
- { field: 'email', message: 'Invalid email' },
296
- ],
297
- },
298
- };
299
-
300
- const error = createErrorFromResponse(response);
301
-
302
- expect(error).toBeInstanceOf(DataApiValidationError);
303
- expect(error.statusCode).toBe(400);
304
- expect((error as DataApiValidationError).validationErrors).toEqual([
305
- { field: 'email', message: 'Invalid email' },
306
- ]);
307
- });
308
-
309
- it('should create ConnectionError for 503 status', () => {
310
- const response = {
311
- status: 503,
312
- message: 'Service unavailable',
313
- config: { url: 'https://api.example.com' },
314
- };
315
-
316
- const error = createErrorFromResponse(response);
317
-
318
- expect(error).toBeInstanceOf(ConnectionError);
319
- expect(error.statusCode).toBe(503);
320
- expect((error as ConnectionError).url).toBe('https://api.example.com');
321
- });
322
-
323
- it('should create ConnectionError for 504 status', () => {
324
- const response = {
325
- status: 504,
326
- message: 'Gateway timeout',
327
- };
328
-
329
- const error = createErrorFromResponse(response);
330
-
331
- expect(error).toBeInstanceOf(ConnectionError);
332
- expect(error.statusCode).toBe(504);
333
- });
334
-
335
- it('should create generic error for unknown status', () => {
336
- const response = {
337
- status: 418,
338
- message: "I'm a teapot",
339
- };
340
-
341
- const error = createErrorFromResponse(response);
342
-
343
- expect(error).toBeInstanceOf(ObjectStackError);
344
- expect(error.code).toBe('UNKNOWN_ERROR');
345
- expect(error.statusCode).toBe(418);
346
- });
347
-
348
- it('should handle response without status code', () => {
349
- const response = {
350
- message: 'Unknown error',
351
- };
352
-
353
- const error = createErrorFromResponse(response);
354
-
355
- expect(error).toBeInstanceOf(ObjectStackError);
356
- expect(error.statusCode).toBe(500);
357
- });
358
-
359
- it('should include context in error details', () => {
360
- const response = {
361
- status: 500,
362
- message: 'Server error',
363
- };
364
-
365
- const error = createErrorFromResponse(response, 'user creation');
366
-
367
- expect(error.details).toHaveProperty('context', 'user creation');
368
- });
369
- });
370
-
371
- describe('isObjectStackError', () => {
372
- it('should return true for ObjectStackError instances', () => {
373
- const error = new ObjectStackError('Test', 'TEST');
374
- expect(isObjectStackError(error)).toBe(true);
375
- });
376
-
377
- it('should return true for derived error classes', () => {
378
- const metadataError = new MetadataNotFoundError('users');
379
- const bulkError = new BulkOperationError('create', 0, 1, []);
380
- const connError = new ConnectionError('timeout');
381
- const authError = new AuthenticationError();
382
- const validError = new DataApiValidationError('invalid');
383
-
384
- expect(isObjectStackError(metadataError)).toBe(true);
385
- expect(isObjectStackError(bulkError)).toBe(true);
386
- expect(isObjectStackError(connError)).toBe(true);
387
- expect(isObjectStackError(authError)).toBe(true);
388
- expect(isObjectStackError(validError)).toBe(true);
389
- });
390
-
391
- it('should return false for regular Error', () => {
392
- const error = new Error('Regular error');
393
- expect(isObjectStackError(error)).toBe(false);
394
- });
395
-
396
- it('should return false for non-error values', () => {
397
- expect(isObjectStackError(null)).toBe(false);
398
- expect(isObjectStackError(undefined)).toBe(false);
399
- expect(isObjectStackError('error')).toBe(false);
400
- expect(isObjectStackError({})).toBe(false);
401
- });
402
- });
403
-
404
- describe('isErrorType', () => {
405
- it('should return true for matching error type', () => {
406
- const error = new MetadataNotFoundError('users');
407
- expect(isErrorType(error, MetadataNotFoundError)).toBe(true);
408
- });
409
-
410
- it('should return false for non-matching error type', () => {
411
- const error = new MetadataNotFoundError('users');
412
- expect(isErrorType(error, DataApiValidationError)).toBe(false);
413
- });
414
-
415
- it('should return true for base class check', () => {
416
- const error = new MetadataNotFoundError('users');
417
- expect(isErrorType(error, ObjectStackError)).toBe(true);
418
- });
419
-
420
- it('should return false for non-error values', () => {
421
- expect(isErrorType(null, ObjectStackError)).toBe(false);
422
- expect(isErrorType(undefined, ObjectStackError)).toBe(false);
423
- expect(isErrorType({}, ObjectStackError)).toBe(false);
424
- });
425
- });
426
- });