@geniee-inc/sfa-kit 1.6.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 (66) hide show
  1. package/GUIDE.md +585 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +31 -0
  4. package/package.json +36 -0
  5. package/src/index.d.ts +29 -0
  6. package/src/index.js +21 -0
  7. package/src/lib/auth/DevToolbar.d.ts +38 -0
  8. package/src/lib/auth/DevToolbar.js +271 -0
  9. package/src/lib/auth/SfaLoginGate.d.ts +53 -0
  10. package/src/lib/auth/SfaLoginGate.js +462 -0
  11. package/src/lib/auth/StandalonePreviewContext.d.ts +27 -0
  12. package/src/lib/auth/StandalonePreviewContext.js +14 -0
  13. package/src/lib/auth/embed-protocol.d.ts +40 -0
  14. package/src/lib/auth/embed-protocol.js +56 -0
  15. package/src/lib/auth/exchange-code.d.ts +14 -0
  16. package/src/lib/auth/exchange-code.js +45 -0
  17. package/src/lib/auth/keystone-auth.d.ts +16 -0
  18. package/src/lib/auth/keystone-auth.js +84 -0
  19. package/src/lib/auth/session-store.d.ts +19 -0
  20. package/src/lib/auth/session-store.js +73 -0
  21. package/src/lib/auth/types.d.ts +33 -0
  22. package/src/lib/auth/types.js +1 -0
  23. package/src/lib/auth/useEmbedContext.d.ts +22 -0
  24. package/src/lib/auth/useEmbedContext.js +31 -0
  25. package/src/lib/components/EmptyState.d.ts +18 -0
  26. package/src/lib/components/EmptyState.js +36 -0
  27. package/src/lib/components/ErrorBoundary.d.ts +31 -0
  28. package/src/lib/components/ErrorBoundary.js +100 -0
  29. package/src/lib/components/FieldRenderer.d.ts +11 -0
  30. package/src/lib/components/FieldRenderer.js +57 -0
  31. package/src/lib/components/RecordList.d.ts +20 -0
  32. package/src/lib/components/RecordList.js +139 -0
  33. package/src/lib/components/RecordView.d.ts +14 -0
  34. package/src/lib/components/RecordView.js +153 -0
  35. package/src/lib/errors.d.ts +15 -0
  36. package/src/lib/errors.js +15 -0
  37. package/src/lib/hooks/useCreateRecord.d.ts +17 -0
  38. package/src/lib/hooks/useCreateRecord.js +48 -0
  39. package/src/lib/hooks/useDeleteRecord.d.ts +16 -0
  40. package/src/lib/hooks/useDeleteRecord.js +48 -0
  41. package/src/lib/hooks/useDescribe.d.ts +16 -0
  42. package/src/lib/hooks/useDescribe.js +40 -0
  43. package/src/lib/hooks/useFieldTypes.d.ts +15 -0
  44. package/src/lib/hooks/useFieldTypes.js +39 -0
  45. package/src/lib/hooks/useObjectCatalog.d.ts +15 -0
  46. package/src/lib/hooks/useObjectCatalog.js +39 -0
  47. package/src/lib/hooks/useRecord.d.ts +15 -0
  48. package/src/lib/hooks/useRecord.js +49 -0
  49. package/src/lib/hooks/useRecordList.d.ts +19 -0
  50. package/src/lib/hooks/useRecordList.js +82 -0
  51. package/src/lib/hooks/useRelatedRecords.d.ts +65 -0
  52. package/src/lib/hooks/useRelatedRecords.js +227 -0
  53. package/src/lib/hooks/useUpdateRecord.d.ts +23 -0
  54. package/src/lib/hooks/useUpdateRecord.js +55 -0
  55. package/src/lib/provider/SfaClient.d.ts +98 -0
  56. package/src/lib/provider/SfaClient.js +688 -0
  57. package/src/lib/provider/SfaProvider.d.ts +38 -0
  58. package/src/lib/provider/SfaProvider.js +44 -0
  59. package/src/lib/provider/types.d.ts +184 -0
  60. package/src/lib/provider/types.js +1 -0
  61. package/src/lib/provider/useSfaClient.d.ts +11 -0
  62. package/src/lib/provider/useSfaClient.js +24 -0
  63. package/src/lib/theme.d.ts +55 -0
  64. package/src/lib/theme.js +68 -0
  65. package/src/lib/version.d.ts +1 -0
  66. package/src/lib/version.js +1 -0
@@ -0,0 +1,688 @@
1
+ import { __awaiter } from "tslib";
2
+ import { ERR_DIRECT_MODE_REQUIRED, ERR_EMPTY_RECORD_ID } from '../errors';
3
+ /** Abort in-flight requests after this many milliseconds. */
4
+ const REQUEST_TIMEOUT_MS = 30000;
5
+ /**
6
+ * @description Returns a timeout AbortSignal, or undefined where the runtime
7
+ * doesn't support AbortSignal.timeout (e.g. older jsdom in tests).
8
+ */
9
+ function timeoutSignal() {
10
+ if (typeof AbortSignal.timeout === 'function') {
11
+ return AbortSignal.timeout(REQUEST_TIMEOUT_MS);
12
+ }
13
+ return undefined;
14
+ }
15
+ /**
16
+ * @description API client for SFA/CRM data operations via keystone.
17
+ * Authenticates with a JWT Bearer token.
18
+ *
19
+ * In direct mode the client talks to keystone's v1 REST API.
20
+ * In proxy mode the client relays through a local Next.js API route.
21
+ */
22
+ export class SfaClient {
23
+ constructor(config) {
24
+ this.apiBase = this.resolveBase(config.apiBase.replace(/\/+$/, ''), config.mode);
25
+ this.auth = config.auth;
26
+ this.mode = config.mode;
27
+ this.organizationId = config.organizationId;
28
+ this.onAuthError = config.onAuthError;
29
+ }
30
+ resolveBase(base, mode) {
31
+ var _a;
32
+ if (mode !== 'proxy' || base.startsWith('http'))
33
+ return base;
34
+ const nextData = typeof globalThis.document !== 'undefined'
35
+ ? globalThis['__NEXT_DATA__']
36
+ : undefined;
37
+ const basePath = (_a = nextData === null || nextData === void 0 ? void 0 : nextData.basePath) !== null && _a !== void 0 ? _a : '';
38
+ if (basePath && !base.startsWith(basePath)) {
39
+ return basePath + base;
40
+ }
41
+ return base;
42
+ }
43
+ // ── Public API ──
44
+ get(collection, recordId) {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ this.assertRecordId(recordId);
47
+ if (this.mode === 'direct') {
48
+ const path = `/v1/objects/${enc(collection)}/records/${enc(recordId)}`;
49
+ const raw = yield this.fetchGet(path);
50
+ return normalizeKeystoneRecord(raw);
51
+ }
52
+ const raw = yield this.fetchGet(`/${collection}/${recordId}`);
53
+ return normalizeRecord(raw);
54
+ });
55
+ }
56
+ list(collection, params) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ var _a, _b, _c;
59
+ const pageSize = (_a = params === null || params === void 0 ? void 0 : params.limit) !== null && _a !== void 0 ? _a : 20;
60
+ const offset = (_b = params === null || params === void 0 ? void 0 : params.offset) !== null && _b !== void 0 ? _b : 0;
61
+ // offset is a record offset, not a page number: pageIndex = floor(offset / pageSize)
62
+ const pageIndex = pageSize > 0 ? Math.floor(offset / pageSize) : 0;
63
+ if (this.mode === 'direct') {
64
+ const body = {
65
+ limit: pageSize,
66
+ };
67
+ if (params === null || params === void 0 ? void 0 : params.cursor) {
68
+ body['cursor'] = params.cursor;
69
+ }
70
+ if (params === null || params === void 0 ? void 0 : params.sortBy) {
71
+ body['sort'] = [
72
+ {
73
+ field: params.sortBy,
74
+ direction: (_c = params.sortOrder) !== null && _c !== void 0 ? _c : 'asc',
75
+ },
76
+ ];
77
+ }
78
+ if (params === null || params === void 0 ? void 0 : params.filter) {
79
+ body['filter'] = params.filter;
80
+ }
81
+ const path = `/v1/objects/${enc(collection)}/records/query`;
82
+ const raw = yield this.postJson(path, body);
83
+ return normalizeKeystoneListResponse(raw, pageIndex, pageSize);
84
+ }
85
+ const query = new URLSearchParams();
86
+ if ((params === null || params === void 0 ? void 0 : params.limit) != null)
87
+ query.set('limit', String(params.limit));
88
+ if ((params === null || params === void 0 ? void 0 : params.offset) != null)
89
+ query.set('offset', String(params.offset));
90
+ if (params === null || params === void 0 ? void 0 : params.sortBy)
91
+ query.set('sort_by', params.sortBy);
92
+ if (params === null || params === void 0 ? void 0 : params.sortOrder)
93
+ query.set('sort_order', params.sortOrder);
94
+ const queryString = query.toString();
95
+ const path = queryString
96
+ ? `/${collection}?${queryString}`
97
+ : `/${collection}`;
98
+ const raw = yield this.fetchGet(path);
99
+ return normalizeListResponse(raw, pageIndex, pageSize);
100
+ });
101
+ }
102
+ describe(objectKey) {
103
+ return __awaiter(this, void 0, void 0, function* () {
104
+ const raw = yield this.fetchGet(this.objectsPath(`/${enc(objectKey)}`));
105
+ return { object: normalizeObjectDetail(raw) };
106
+ });
107
+ }
108
+ listObjects() {
109
+ return __awaiter(this, void 0, void 0, function* () {
110
+ var _a;
111
+ const raw = yield this.fetchGet(this.objectsPath());
112
+ const data = raw;
113
+ const objects = ((_a = data.objects) !== null && _a !== void 0 ? _a : []).map(o => normalizeObjectSummary(o));
114
+ return { objects };
115
+ });
116
+ }
117
+ listFieldTypes() {
118
+ return __awaiter(this, void 0, void 0, function* () {
119
+ var _a;
120
+ const raw = yield this.fetchGet(this.fieldTypesPath());
121
+ const data = raw;
122
+ const fieldTypes = ((_a = data.field_types) !== null && _a !== void 0 ? _a : []).map(ft => normalizeFieldTypeDescriptor(ft));
123
+ return { fieldTypes };
124
+ });
125
+ }
126
+ // ── Mutations (direct mode only) ──
127
+ assertDirectMode() {
128
+ if (this.mode !== 'direct') {
129
+ throw new Error(ERR_DIRECT_MODE_REQUIRED);
130
+ }
131
+ }
132
+ /** @description Throws if recordId is empty to prevent malformed API requests. */
133
+ assertRecordId(recordId) {
134
+ if (!recordId) {
135
+ throw new Error(ERR_EMPTY_RECORD_ID);
136
+ }
137
+ }
138
+ /**
139
+ * @description Creates a new record. Optionally accepts an idempotency key
140
+ * to prevent duplicate creates on retries.
141
+ */
142
+ create(collection, fields, options) {
143
+ return __awaiter(this, void 0, void 0, function* () {
144
+ this.assertDirectMode();
145
+ const path = `/v1/objects/${enc(collection)}/records`;
146
+ const extra = {};
147
+ if (options === null || options === void 0 ? void 0 : options.idempotencyKey) {
148
+ extra['Idempotency-Key'] = options.idempotencyKey;
149
+ }
150
+ const response = yield this.fetchWithBody('POST', path, { fields: normalizeWriteFields(fields) }, extra);
151
+ const raw = yield response.json();
152
+ return normalizeKeystoneRecord(raw);
153
+ });
154
+ }
155
+ /**
156
+ * @description Updates a record with optimistic locking. The version token
157
+ * (from a prior GET) is sent as the If-Match header; a 412 response means
158
+ * another write landed first.
159
+ */
160
+ update(collection, recordId, fields, version) {
161
+ return __awaiter(this, void 0, void 0, function* () {
162
+ this.assertDirectMode();
163
+ this.assertRecordId(recordId);
164
+ const path = `/v1/objects/${enc(collection)}/records/${enc(recordId)}`;
165
+ const response = yield this.fetchWithBody('PATCH', path, { fields: normalizeWriteFields(fields) }, { 'If-Match': version });
166
+ const raw = yield response.json();
167
+ return normalizeKeystoneRecord(raw);
168
+ });
169
+ }
170
+ /**
171
+ * @description Deletes a record with optimistic locking. Returns void on
172
+ * success (204). Throws SfaConflictError on 412 if the record was modified
173
+ * since the version was obtained.
174
+ */
175
+ delete(collection, recordId, version) {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ this.assertDirectMode();
178
+ this.assertRecordId(recordId);
179
+ const path = `/v1/objects/${enc(collection)}/records/${enc(recordId)}`;
180
+ yield this.fetchWithBody('DELETE', path, undefined, {
181
+ 'If-Match': version,
182
+ });
183
+ });
184
+ }
185
+ // ── Path resolution ──
186
+ /** @description Returns the objects metadata path for the current mode. */
187
+ objectsPath(suffix = '') {
188
+ const prefix = this.mode === 'direct' ? '/v1/objects' : '/objects';
189
+ return `${prefix}${suffix}`;
190
+ }
191
+ /** @description Returns the field-types catalog path for the current mode. */
192
+ fieldTypesPath() {
193
+ return this.mode === 'direct' ? '/v1/field-types' : '/field-types';
194
+ }
195
+ // ── Private transport ──
196
+ applyAuth(headers) {
197
+ var _a;
198
+ if ((_a = this.auth) === null || _a === void 0 ? void 0 : _a.accessToken) {
199
+ headers['Authorization'] = `Bearer ${this.auth.accessToken}`;
200
+ }
201
+ if (this.organizationId) {
202
+ headers['organization-id'] = this.organizationId;
203
+ }
204
+ }
205
+ /**
206
+ * @description GET request with auth headers. Works for all read-only
207
+ * endpoints in both direct and proxy modes.
208
+ */
209
+ fetchGet(path) {
210
+ return __awaiter(this, void 0, void 0, function* () {
211
+ var _a;
212
+ const url = `${this.apiBase}${path}`;
213
+ const headers = {
214
+ 'Content-Type': 'application/json',
215
+ };
216
+ this.applyAuth(headers);
217
+ const response = yield globalThis.fetch(url, {
218
+ method: 'GET',
219
+ headers,
220
+ signal: timeoutSignal(),
221
+ });
222
+ if (!response.ok) {
223
+ if (response.status === 401)
224
+ (_a = this.onAuthError) === null || _a === void 0 ? void 0 : _a.call(this);
225
+ const text = yield response.text().catch(() => '');
226
+ throw new SfaApiError(response.status, response.statusText, text, url);
227
+ }
228
+ return response.json();
229
+ });
230
+ }
231
+ /**
232
+ * @description POST request with auth headers and a JSON body.
233
+ * Body is sent directly (not wrapped in { data: ... }).
234
+ */
235
+ postJson(path, body) {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ var _a;
238
+ const url = `${this.apiBase}${path}`;
239
+ const headers = {
240
+ 'Content-Type': 'application/json',
241
+ };
242
+ this.applyAuth(headers);
243
+ const response = yield globalThis.fetch(url, {
244
+ method: 'POST',
245
+ headers,
246
+ body: JSON.stringify(body),
247
+ signal: timeoutSignal(),
248
+ });
249
+ if (!response.ok) {
250
+ if (response.status === 401)
251
+ (_a = this.onAuthError) === null || _a === void 0 ? void 0 : _a.call(this);
252
+ const text = yield response.text().catch(() => '');
253
+ throw new SfaApiError(response.status, response.statusText, text, url);
254
+ }
255
+ return response.json();
256
+ });
257
+ }
258
+ /**
259
+ * @description Generic fetch with a configurable HTTP method and optional
260
+ * body / extra headers. Returns the raw Response so callers can inspect
261
+ * status codes, headers, or handle empty bodies (204).
262
+ */
263
+ fetchWithBody(method, path, body, extraHeaders) {
264
+ return __awaiter(this, void 0, void 0, function* () {
265
+ var _a;
266
+ const url = `${this.apiBase}${path}`;
267
+ const headers = Object.assign({ 'Content-Type': 'application/json' }, extraHeaders);
268
+ this.applyAuth(headers);
269
+ const response = yield globalThis.fetch(url, {
270
+ method,
271
+ headers,
272
+ body: body ? JSON.stringify(body) : undefined,
273
+ signal: timeoutSignal(),
274
+ });
275
+ if (!response.ok) {
276
+ if (response.status === 401)
277
+ (_a = this.onAuthError) === null || _a === void 0 ? void 0 : _a.call(this);
278
+ const text = yield response.text().catch(() => '');
279
+ if (response.status === 412) {
280
+ throw SfaConflictError.fromBody(response.status, response.statusText, text, url);
281
+ }
282
+ if (response.status === 422) {
283
+ throw SfaValidationError.fromBody(response.status, response.statusText, text, url);
284
+ }
285
+ throw new SfaApiError(response.status, response.statusText, text, url);
286
+ }
287
+ return response;
288
+ });
289
+ }
290
+ }
291
+ /** @description URI-encode a path segment. */
292
+ function enc(segment) {
293
+ return encodeURIComponent(segment);
294
+ }
295
+ // ── Keystone record normalization (direct mode) ──
296
+ /**
297
+ * @description Converts a caller-supplied fields map into the raw values
298
+ * expected by Keystone's RecordWrite body. If a value is already a raw value
299
+ * it is preserved; if it is an SfaFieldValue object (with kind/value/label)
300
+ * only the stored value is sent, so labels are never written back.
301
+ */
302
+ function normalizeWriteFields(fields) {
303
+ var _a;
304
+ const result = {};
305
+ for (const [key, raw] of Object.entries(fields)) {
306
+ result[key] = isSfaFieldValue(raw) ? (_a = raw.value) !== null && _a !== void 0 ? _a : null : raw;
307
+ }
308
+ return result;
309
+ }
310
+ function isSfaFieldValue(value) {
311
+ return (typeof value === 'object' &&
312
+ value != null &&
313
+ 'kind' in value &&
314
+ 'value' in value &&
315
+ typeof value.kind === 'string');
316
+ }
317
+ /**
318
+ * @description Normalizes a CrmRecord from keystone v1 API into the
319
+ * internal SfaRecord shape used by all sfa-kit components.
320
+ */
321
+ function normalizeKeystoneRecord(raw) {
322
+ var _a, _b, _c, _d, _e, _f, _g, _h;
323
+ const data = raw;
324
+ const perms = (_a = data['permissions']) !== null && _a !== void 0 ? _a : {};
325
+ return {
326
+ record_id: (_b = data['id']) !== null && _b !== void 0 ? _b : '',
327
+ collection_name: (_c = data['object_key']) !== null && _c !== void 0 ? _c : '',
328
+ fields: normalizeKeystoneFields(data['fields']),
329
+ titleFields: [],
330
+ title: (_d = data['title']) !== null && _d !== void 0 ? _d : null,
331
+ version: (_e = data['version']) !== null && _e !== void 0 ? _e : null,
332
+ permissions: {
333
+ canEdit: (_g = (_f = perms['editable']) !== null && _f !== void 0 ? _f : perms['updatable']) !== null && _g !== void 0 ? _g : false,
334
+ canDelete: (_h = perms['deletable']) !== null && _h !== void 0 ? _h : false,
335
+ },
336
+ };
337
+ }
338
+ function normalizeKeystoneFields(rawFields) {
339
+ if (!rawFields || typeof rawFields !== 'object')
340
+ return {};
341
+ const result = {};
342
+ for (const [key, raw] of Object.entries(rawFields)) {
343
+ if (raw && typeof raw === 'object') {
344
+ result[key] = normalizeKeystoneCell(raw);
345
+ }
346
+ else {
347
+ // Bare scalar or null: preserve the field instead of silently dropping it
348
+ result[key] = {
349
+ kind: inferScalarKind(raw),
350
+ value: raw !== null && raw !== void 0 ? raw : null,
351
+ label: raw != null ? String(raw) : null,
352
+ };
353
+ }
354
+ }
355
+ return result;
356
+ }
357
+ function normalizeKeystoneCell(cell) {
358
+ var _a, _b, _c, _d, _e;
359
+ const shape = cell['shape'];
360
+ switch (shape) {
361
+ case 'coded':
362
+ return {
363
+ kind: 'single_choice',
364
+ value: (_a = cell['value']) !== null && _a !== void 0 ? _a : null,
365
+ label: (_b = cell['label']) !== null && _b !== void 0 ? _b : null,
366
+ };
367
+ case 'coded_list': {
368
+ const labels = cell['label'];
369
+ return {
370
+ kind: 'multi_choice',
371
+ value: (_c = cell['value']) !== null && _c !== void 0 ? _c : [],
372
+ label: Array.isArray(labels)
373
+ ? labels.filter(Boolean).join(', ')
374
+ : null,
375
+ };
376
+ }
377
+ case 'scalar':
378
+ return {
379
+ kind: inferScalarKind(cell['value']),
380
+ value: (_d = cell['value']) !== null && _d !== void 0 ? _d : null,
381
+ label: cell['value'] != null ? String(cell['value']) : null,
382
+ };
383
+ case 'unresolved':
384
+ default:
385
+ return {
386
+ kind: 'text',
387
+ value: (_e = cell['value']) !== null && _e !== void 0 ? _e : null,
388
+ label: cell['value'] != null ? String(cell['value']) : null,
389
+ };
390
+ }
391
+ }
392
+ function inferScalarKind(value) {
393
+ if (typeof value === 'number')
394
+ return 'number';
395
+ if (typeof value === 'string') {
396
+ if (/^\d{4}-\d{2}-\d{2}T/.test(value))
397
+ return 'datetime';
398
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value))
399
+ return 'date';
400
+ }
401
+ return 'text';
402
+ }
403
+ function normalizeKeystoneListResponse(raw, pageIndex, pageSize) {
404
+ var _a, _b, _c;
405
+ const data = raw;
406
+ const rawRecords = (_a = data['records']) !== null && _a !== void 0 ? _a : [];
407
+ const items = rawRecords.map(r => normalizeKeystoneRecord(r));
408
+ const nextCursor = (_b = data['next_cursor']) !== null && _b !== void 0 ? _b : null;
409
+ const prevCursor = (_c = data['prev_cursor']) !== null && _c !== void 0 ? _c : null;
410
+ const hasMore = nextCursor != null;
411
+ // Keystone uses cursor pagination without a total count.
412
+ // Estimate a totalCount that keeps the hasMore logic correct.
413
+ const totalCount = hasMore
414
+ ? (pageIndex + 1) * pageSize + 1
415
+ : pageIndex * pageSize + items.length;
416
+ return { items, totalCount, hasMore, pageIndex, nextCursor, prevCursor };
417
+ }
418
+ // ── Legacy Chikyu normalization (proxy mode) ──
419
+ const CHIKYU_KIND_MAP = {
420
+ CHIKYU_STRING: 'text',
421
+ CHIKYU_TEXT: 'text',
422
+ CHIKYU_NUMBER: 'number',
423
+ CHIKYU_DATE: 'date',
424
+ CHIKYU_DATE_TIME: 'datetime',
425
+ CHIKYU_SELECT: 'single_choice',
426
+ CHIKYU_MULTIPLE_SELECT: 'multi_choice',
427
+ CHIKYU_USER_SELECT: 'user_reference',
428
+ CHIKYU_MULTIPLE_USER_SELECT: 'user_reference',
429
+ CHIKYU_COLLECTION: 'reference',
430
+ CHIKYU_SUB_COLLECTION: 'reference',
431
+ CHIKYU_ID_LIST: 'reference',
432
+ CHIKYU_DATE_DIFF: 'computed',
433
+ CHIKYU_AUTO_NUMBERING: 'computed',
434
+ CHIKYU_FORMULA: 'computed',
435
+ CHIKYU_LOOKUP: 'computed',
436
+ CHIKYU_HEAP_AGGREGATE: 'computed',
437
+ CHIKYU_ROLE: 'user_reference',
438
+ CHIKYU_LAYOUT_TYPE: 'record_type',
439
+ CHIKYU_IMAGE_HTML: 'text',
440
+ };
441
+ function normalizeFieldValue(raw) {
442
+ var _a, _b, _c, _d;
443
+ const chikyuType = (_a = raw['type']) !== null && _a !== void 0 ? _a : '';
444
+ return {
445
+ kind: (_b = CHIKYU_KIND_MAP[chikyuType]) !== null && _b !== void 0 ? _b : 'text',
446
+ value: (_c = raw['value']) !== null && _c !== void 0 ? _c : null,
447
+ label: (_d = raw['caption']) !== null && _d !== void 0 ? _d : null,
448
+ };
449
+ }
450
+ function normalizeFields(rawMapping) {
451
+ if (!rawMapping || typeof rawMapping !== 'object')
452
+ return {};
453
+ const result = {};
454
+ for (const [key, raw] of Object.entries(rawMapping)) {
455
+ if (raw && typeof raw === 'object') {
456
+ result[key] = normalizeFieldValue(raw);
457
+ }
458
+ }
459
+ return result;
460
+ }
461
+ function normalizeRecord(raw) {
462
+ var _a, _b, _c, _d, _e, _f;
463
+ const data = raw;
464
+ return {
465
+ record_id: (_b = (_a = data['record_id']) !== null && _a !== void 0 ? _a : data['_id']) !== null && _b !== void 0 ? _b : '',
466
+ collection_name: (_c = data['collection_name']) !== null && _c !== void 0 ? _c : '',
467
+ fields: normalizeFields(data['field_value_mapping']),
468
+ titleFields: (_d = data['title_field_name_list']) !== null && _d !== void 0 ? _d : [],
469
+ permissions: {
470
+ canEdit: (_e = data['is_editable']) !== null && _e !== void 0 ? _e : false,
471
+ canDelete: (_f = data['is_deletable']) !== null && _f !== void 0 ? _f : false,
472
+ },
473
+ };
474
+ }
475
+ function normalizeListResponse(raw, pageIndex, pageSize) {
476
+ var _a, _b, _c, _d;
477
+ const data = raw;
478
+ const rawItems = (_b = (_a = data['item_list']) !== null && _a !== void 0 ? _a : data['items']) !== null && _b !== void 0 ? _b : [];
479
+ const items = rawItems.map(item => normalizeRecord(item));
480
+ const totalCount = (_d = (_c = data['total_count']) !== null && _c !== void 0 ? _c : data['totalCount']) !== null && _d !== void 0 ? _d : items.length;
481
+ return {
482
+ items,
483
+ totalCount,
484
+ hasMore: (pageIndex + 1) * pageSize < totalCount,
485
+ pageIndex,
486
+ };
487
+ }
488
+ const VALID_KINDS = new Set([
489
+ 'text',
490
+ 'number',
491
+ 'date',
492
+ 'datetime',
493
+ 'single_choice',
494
+ 'multi_choice',
495
+ 'reference',
496
+ 'user_reference',
497
+ 'computed',
498
+ 'record_type',
499
+ ]);
500
+ const VALID_CARDINALITIES = new Set([
501
+ 'one',
502
+ 'many',
503
+ ]);
504
+ const VALID_REL_KINDS = new Set([
505
+ 'association',
506
+ 'master_detail',
507
+ ]);
508
+ function normalizeRelationship(raw) {
509
+ var _a, _b, _c, _d, _e;
510
+ return {
511
+ name: (_a = raw.name) !== null && _a !== void 0 ? _a : '',
512
+ target: (_b = raw.target) !== null && _b !== void 0 ? _b : '',
513
+ cardinality: VALID_CARDINALITIES.has((_c = raw.cardinality) !== null && _c !== void 0 ? _c : '')
514
+ ? raw.cardinality
515
+ : 'one',
516
+ kind: VALID_REL_KINDS.has((_d = raw.kind) !== null && _d !== void 0 ? _d : '')
517
+ ? raw.kind
518
+ : 'association',
519
+ viaField: (_e = raw.via_field) !== null && _e !== void 0 ? _e : '',
520
+ };
521
+ }
522
+ function normalizeFieldDescriptor(raw) {
523
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
524
+ const kind = VALID_KINDS.has((_a = raw.kind) !== null && _a !== void 0 ? _a : '')
525
+ ? raw.kind
526
+ : 'text';
527
+ const descriptor = Object.assign(Object.assign(Object.assign(Object.assign({ key: (_b = raw.key) !== null && _b !== void 0 ? _b : '', label: (_d = (_c = raw.label) !== null && _c !== void 0 ? _c : raw.key) !== null && _d !== void 0 ? _d : '', kind, description: (_e = raw.description) !== null && _e !== void 0 ? _e : null, required: (_f = raw.required) !== null && _f !== void 0 ? _f : false, readOnly: (_g = raw.read_only) !== null && _g !== void 0 ? _g : false, order: (_h = raw.order) !== null && _h !== void 0 ? _h : 0, section: (_j = raw.section) !== null && _j !== void 0 ? _j : null, filterable: (_k = raw.filterable) !== null && _k !== void 0 ? _k : false, sortable: (_l = raw.sortable) !== null && _l !== void 0 ? _l : false }, (raw.options ? { options: raw.options } : {})), (raw.target_object ? { targetObject: raw.target_object } : {})), (raw.cardinality && VALID_CARDINALITIES.has(raw.cardinality)
528
+ ? { cardinality: raw.cardinality }
529
+ : {})), (raw.display_field ? { displayField: raw.display_field } : {}));
530
+ return descriptor;
531
+ }
532
+ function normalizePermissions(raw) {
533
+ var _a, _b, _c, _d;
534
+ return {
535
+ readable: (_a = raw === null || raw === void 0 ? void 0 : raw.readable) !== null && _a !== void 0 ? _a : false,
536
+ creatable: (_b = raw === null || raw === void 0 ? void 0 : raw.creatable) !== null && _b !== void 0 ? _b : false,
537
+ updatable: (_c = raw === null || raw === void 0 ? void 0 : raw.updatable) !== null && _c !== void 0 ? _c : false,
538
+ deletable: (_d = raw === null || raw === void 0 ? void 0 : raw.deletable) !== null && _d !== void 0 ? _d : false,
539
+ };
540
+ }
541
+ function normalizeObjectSummary(raw) {
542
+ var _a, _b, _c, _d, _e;
543
+ return {
544
+ key: (_a = raw.key) !== null && _a !== void 0 ? _a : '',
545
+ label: (_c = (_b = raw.label) !== null && _b !== void 0 ? _b : raw.key) !== null && _c !== void 0 ? _c : '',
546
+ origin: raw.origin === 'custom' ? 'custom' : 'standard',
547
+ primaryDisplayField: (_d = raw.primary_display_field) !== null && _d !== void 0 ? _d : null,
548
+ relationships: ((_e = raw.relationships) !== null && _e !== void 0 ? _e : []).map(normalizeRelationship),
549
+ };
550
+ }
551
+ function normalizeObjectDetail(raw) {
552
+ var _a, _b, _c, _d, _e, _f;
553
+ return {
554
+ key: (_a = raw.key) !== null && _a !== void 0 ? _a : '',
555
+ label: (_c = (_b = raw.label) !== null && _b !== void 0 ? _b : raw.key) !== null && _c !== void 0 ? _c : '',
556
+ origin: raw.origin === 'custom' ? 'custom' : 'standard',
557
+ primaryDisplayField: (_d = raw.primary_display_field) !== null && _d !== void 0 ? _d : null,
558
+ fields: ((_e = raw.fields) !== null && _e !== void 0 ? _e : []).map(normalizeFieldDescriptor),
559
+ relationships: ((_f = raw.relationships) !== null && _f !== void 0 ? _f : []).map(normalizeRelationship),
560
+ permissions: normalizePermissions(raw.permissions),
561
+ };
562
+ }
563
+ function normalizeFieldTypeDescriptor(raw) {
564
+ var _a, _b, _c, _d, _e, _f, _g, _h;
565
+ return {
566
+ kind: VALID_KINDS.has((_a = raw.kind) !== null && _a !== void 0 ? _a : '') ? raw.kind : 'text',
567
+ summary: (_b = raw.summary) !== null && _b !== void 0 ? _b : '',
568
+ valueShape: (_c = raw.value_shape) !== null && _c !== void 0 ? _c : '',
569
+ multiValued: (_d = raw.multi_valued) !== null && _d !== void 0 ? _d : false,
570
+ editableByDefault: (_e = raw.editable_by_default) !== null && _e !== void 0 ? _e : true,
571
+ hasOptions: (_f = raw.has_options) !== null && _f !== void 0 ? _f : false,
572
+ referencesObject: (_g = raw.references_object) !== null && _g !== void 0 ? _g : false,
573
+ operators: ((_h = raw.operators) !== null && _h !== void 0 ? _h : []),
574
+ };
575
+ }
576
+ /** Max characters of the response body retained on SfaApiError. Bodies can
577
+ * contain record data, so keep only enough for debugging. */
578
+ const ERROR_BODY_MAX_LENGTH = 500;
579
+ /**
580
+ * @description Error from the SFA API with parsed Keystone error details.
581
+ * Keystone returns structured errors with `detail`, `code`, and `title` fields.
582
+ * The message prefers the human-readable `detail` over the raw status text.
583
+ * The raw body is truncated to limit exposure of record data via error
584
+ * monitoring services.
585
+ */
586
+ export class SfaApiError extends Error {
587
+ constructor(status, statusText, body, url) {
588
+ var _a, _b, _c;
589
+ const parsed = parseKeystoneError(body);
590
+ const displayMessage = (_a = parsed === null || parsed === void 0 ? void 0 : parsed.detail) !== null && _a !== void 0 ? _a : (statusText || `HTTP ${status} error`);
591
+ super(`SFA API ${status}\u{FF1A}${displayMessage}`);
592
+ this.status = status;
593
+ this.statusText = statusText;
594
+ this.url = url;
595
+ this.name = 'SfaApiError';
596
+ this.detail = (_b = parsed === null || parsed === void 0 ? void 0 : parsed.detail) !== null && _b !== void 0 ? _b : null;
597
+ this.code = (_c = parsed === null || parsed === void 0 ? void 0 : parsed.code) !== null && _c !== void 0 ? _c : null;
598
+ this.body = body.slice(0, ERROR_BODY_MAX_LENGTH);
599
+ }
600
+ }
601
+ function parseKeystoneError(body) {
602
+ var _a, _b;
603
+ if (!body)
604
+ return null;
605
+ try {
606
+ const parsed = JSON.parse(body);
607
+ if (typeof parsed['detail'] === 'string') {
608
+ return {
609
+ detail: parsed['detail'],
610
+ code: (_a = parsed['code']) !== null && _a !== void 0 ? _a : '',
611
+ title: (_b = parsed['title']) !== null && _b !== void 0 ? _b : undefined,
612
+ };
613
+ }
614
+ }
615
+ catch (_c) {
616
+ // Not JSON or missing detail field
617
+ }
618
+ return null;
619
+ }
620
+ // ── Mutation error helpers ──
621
+ function parseConflictRecord(body) {
622
+ var _a, _b;
623
+ try {
624
+ const parsed = JSON.parse(body);
625
+ const recordData = (_b = (_a = parsed['current_record']) !== null && _a !== void 0 ? _a : parsed['record']) !== null && _b !== void 0 ? _b : parsed['current'];
626
+ if (recordData && typeof recordData === 'object') {
627
+ return normalizeKeystoneRecord(recordData);
628
+ }
629
+ }
630
+ catch (_c) {
631
+ // Body may not contain the current record
632
+ }
633
+ return null;
634
+ }
635
+ function parseFieldErrors(body) {
636
+ try {
637
+ const parsed = JSON.parse(body);
638
+ const rawErrors = parsed['errors'];
639
+ if (Array.isArray(rawErrors)) {
640
+ return rawErrors
641
+ .filter((e) => e != null && typeof e === 'object')
642
+ .map(e => {
643
+ var _a, _b, _c;
644
+ return ({
645
+ field: (_a = e['field']) !== null && _a !== void 0 ? _a : '',
646
+ code: (_b = e['code']) !== null && _b !== void 0 ? _b : '',
647
+ message: (_c = e['message']) !== null && _c !== void 0 ? _c : '',
648
+ });
649
+ });
650
+ }
651
+ }
652
+ catch (_a) {
653
+ // Body may not be parseable
654
+ }
655
+ return [];
656
+ }
657
+ // ── Mutation error classes ──
658
+ /**
659
+ * @description Thrown on 412 Precondition Failed. The record was modified by
660
+ * another writer since the caller's version was obtained. The response body
661
+ * contains the current record so the UI can show what changed.
662
+ */
663
+ export class SfaConflictError extends SfaApiError {
664
+ constructor(status, statusText, body, url, currentRecord) {
665
+ super(status, statusText, body, url);
666
+ this.name = 'SfaConflictError';
667
+ this.currentRecord = currentRecord;
668
+ }
669
+ static fromBody(status, statusText, body, url) {
670
+ const currentRecord = parseConflictRecord(body);
671
+ return new SfaConflictError(status, statusText, body, url, currentRecord);
672
+ }
673
+ }
674
+ /**
675
+ * @description Thrown on 422 with code VALIDATION_FAILED. Exposes per-field
676
+ * errors so the UI can highlight which fields need correction.
677
+ */
678
+ export class SfaValidationError extends SfaApiError {
679
+ constructor(status, statusText, body, url, fieldErrors) {
680
+ super(status, statusText, body, url);
681
+ this.name = 'SfaValidationError';
682
+ this.fieldErrors = fieldErrors;
683
+ }
684
+ static fromBody(status, statusText, body, url) {
685
+ const fieldErrors = parseFieldErrors(body);
686
+ return new SfaValidationError(status, statusText, body, url, fieldErrors);
687
+ }
688
+ }