@geniee-inc/sfa-kit 1.8.0 → 1.9.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
|
@@ -12,11 +12,18 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
12
12
|
private readonly organizationId;
|
|
13
13
|
private readonly onAuthError;
|
|
14
14
|
readonly mode: SfaApiMode;
|
|
15
|
+
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
16
|
+
* so concurrent callers share a single in-flight request. */
|
|
17
|
+
private readonly describeCache;
|
|
15
18
|
constructor(config: SfaClientConfig);
|
|
16
19
|
private resolveBase;
|
|
17
20
|
get(collection: string, recordId: string): Promise<SfaRecord>;
|
|
18
21
|
list(collection: string, params?: SfaListParams): Promise<SfaListResponse>;
|
|
19
22
|
describe(objectKey: string): Promise<SfaDescribeResult>;
|
|
23
|
+
/** Cached describe fetch. Returns a shared Promise so concurrent callers
|
|
24
|
+
* for the same objectKey deduplicate into one HTTP request. On failure the
|
|
25
|
+
* entry is evicted so the next caller retries. */
|
|
26
|
+
private describeObject;
|
|
20
27
|
listObjects(): Promise<SfaObjectCatalog>;
|
|
21
28
|
listFieldTypes(): Promise<SfaFieldTypeCatalog>;
|
|
22
29
|
private assertDirectMode;
|
|
@@ -33,6 +40,12 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
33
40
|
* another write landed first.
|
|
34
41
|
*/
|
|
35
42
|
update(collection: string, recordId: string, fields: Record<string, unknown>, version: string): Promise<SfaRecord>;
|
|
43
|
+
/**
|
|
44
|
+
* @description Fetches describe metadata (with cache) and normalizes
|
|
45
|
+
* cardinality:"many" reference fields to arrays. Falls back to no-op if
|
|
46
|
+
* describe fails, so a broken metadata endpoint never blocks writes.
|
|
47
|
+
*/
|
|
48
|
+
private applySchemaAwareNormalization;
|
|
36
49
|
/**
|
|
37
50
|
* @description Deletes a record with optimistic locking. Returns void on
|
|
38
51
|
* success (204). Throws SfaConflictError on 412 if the record was modified
|
|
@@ -21,6 +21,9 @@ function timeoutSignal() {
|
|
|
21
21
|
*/
|
|
22
22
|
export class SfaClient {
|
|
23
23
|
constructor(config) {
|
|
24
|
+
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
25
|
+
* so concurrent callers share a single in-flight request. */
|
|
26
|
+
this.describeCache = new Map();
|
|
24
27
|
this.apiBase = this.resolveBase(config.apiBase.replace(/\/+$/, ''), config.mode);
|
|
25
28
|
this.auth = config.auth;
|
|
26
29
|
this.mode = config.mode;
|
|
@@ -101,10 +104,26 @@ export class SfaClient {
|
|
|
101
104
|
}
|
|
102
105
|
describe(objectKey) {
|
|
103
106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
-
const
|
|
105
|
-
return { object
|
|
107
|
+
const object = yield this.describeObject(objectKey);
|
|
108
|
+
return { object };
|
|
106
109
|
});
|
|
107
110
|
}
|
|
111
|
+
/** Cached describe fetch. Returns a shared Promise so concurrent callers
|
|
112
|
+
* for the same objectKey deduplicate into one HTTP request. On failure the
|
|
113
|
+
* entry is evicted so the next caller retries. */
|
|
114
|
+
describeObject(objectKey) {
|
|
115
|
+
const cached = this.describeCache.get(objectKey);
|
|
116
|
+
if (cached)
|
|
117
|
+
return cached;
|
|
118
|
+
const promise = this.fetchGet(this.objectsPath(`/${enc(objectKey)}`))
|
|
119
|
+
.then(raw => normalizeObjectDetail(raw))
|
|
120
|
+
.catch((err) => {
|
|
121
|
+
this.describeCache.delete(objectKey);
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
this.describeCache.set(objectKey, promise);
|
|
125
|
+
return promise;
|
|
126
|
+
}
|
|
108
127
|
listObjects() {
|
|
109
128
|
return __awaiter(this, void 0, void 0, function* () {
|
|
110
129
|
var _a;
|
|
@@ -142,12 +161,13 @@ export class SfaClient {
|
|
|
142
161
|
create(collection, fields, options) {
|
|
143
162
|
return __awaiter(this, void 0, void 0, function* () {
|
|
144
163
|
this.assertDirectMode();
|
|
164
|
+
const normalized = yield this.applySchemaAwareNormalization(collection, fields);
|
|
145
165
|
const path = `/v1/objects/${enc(collection)}/records`;
|
|
146
166
|
const extra = {};
|
|
147
167
|
if (options === null || options === void 0 ? void 0 : options.idempotencyKey) {
|
|
148
168
|
extra['Idempotency-Key'] = options.idempotencyKey;
|
|
149
169
|
}
|
|
150
|
-
const response = yield this.fetchWithBody('POST', path, { fields: normalizeWriteFields(
|
|
170
|
+
const response = yield this.fetchWithBody('POST', path, { fields: normalizeWriteFields(normalized) }, extra);
|
|
151
171
|
const raw = yield response.json();
|
|
152
172
|
return normalizeKeystoneRecord(raw);
|
|
153
173
|
});
|
|
@@ -161,12 +181,32 @@ export class SfaClient {
|
|
|
161
181
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
182
|
this.assertDirectMode();
|
|
163
183
|
this.assertRecordId(recordId);
|
|
184
|
+
const normalized = yield this.applySchemaAwareNormalization(collection, fields);
|
|
164
185
|
const path = `/v1/objects/${enc(collection)}/records/${enc(recordId)}`;
|
|
165
|
-
const response = yield this.fetchWithBody('PATCH', path, { fields: normalizeWriteFields(
|
|
186
|
+
const response = yield this.fetchWithBody('PATCH', path, { fields: normalizeWriteFields(normalized) }, { 'If-Match': version });
|
|
166
187
|
const raw = yield response.json();
|
|
167
188
|
return normalizeKeystoneRecord(raw);
|
|
168
189
|
});
|
|
169
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* @description Fetches describe metadata (with cache) and normalizes
|
|
193
|
+
* cardinality:"many" reference fields to arrays. Falls back to no-op if
|
|
194
|
+
* describe fails, so a broken metadata endpoint never blocks writes.
|
|
195
|
+
*/
|
|
196
|
+
applySchemaAwareNormalization(collection, fields) {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
try {
|
|
199
|
+
const detail = yield this.describeObject(collection);
|
|
200
|
+
return normalizeCardinalityFields(fields, detail.fields);
|
|
201
|
+
}
|
|
202
|
+
catch (_a) {
|
|
203
|
+
// Describe failed -- proceed without cardinality normalization.
|
|
204
|
+
// The write will either succeed (no many-cardinality fields in payload)
|
|
205
|
+
// or fail with the same 422 it would have before this feature.
|
|
206
|
+
return fields;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
170
210
|
/**
|
|
171
211
|
* @description Deletes a record with optimistic locking. Returns void on
|
|
172
212
|
* success (204). Throws SfaConflictError on 412 if the record was modified
|
|
@@ -293,6 +333,49 @@ function enc(segment) {
|
|
|
293
333
|
return encodeURIComponent(segment);
|
|
294
334
|
}
|
|
295
335
|
// ── Keystone record normalization (direct mode) ──
|
|
336
|
+
// ── Write-path normalization ──
|
|
337
|
+
/** Field kinds whose values must be arrays when cardinality is 'many'. */
|
|
338
|
+
const ARRAY_WRAPPABLE_KINDS = new Set([
|
|
339
|
+
'reference',
|
|
340
|
+
'user_reference',
|
|
341
|
+
]);
|
|
342
|
+
/**
|
|
343
|
+
* @description Wraps scalar values in arrays for fields whose schema declares
|
|
344
|
+
* cardinality:"many". Already-array values, null, and undefined are passed
|
|
345
|
+
* through unchanged. Runs *before* normalizeWriteFields so SfaFieldValue
|
|
346
|
+
* wrappers are still intact; when a wrapper is detected its inner `.value`
|
|
347
|
+
* is wrapped while the wrapper structure is preserved.
|
|
348
|
+
*/
|
|
349
|
+
function normalizeCardinalityFields(fields, descriptors) {
|
|
350
|
+
const manyFields = new Set();
|
|
351
|
+
for (const desc of descriptors) {
|
|
352
|
+
if (desc.cardinality === 'many' && ARRAY_WRAPPABLE_KINDS.has(desc.kind)) {
|
|
353
|
+
manyFields.add(desc.key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (manyFields.size === 0)
|
|
357
|
+
return fields;
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
360
|
+
if (manyFields.has(key) && value != null && !Array.isArray(value)) {
|
|
361
|
+
if (isSfaFieldValue(value)) {
|
|
362
|
+
// Wrap the inner value; preserve the SfaFieldValue wrapper so the
|
|
363
|
+
// subsequent normalizeWriteFields step can still unwrap it.
|
|
364
|
+
const inner = value.value;
|
|
365
|
+
result[key] =
|
|
366
|
+
inner != null && !Array.isArray(inner)
|
|
367
|
+
? Object.assign(Object.assign({}, value), { value: [inner] }) : value;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
result[key] = [value];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
result[key] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
296
379
|
/**
|
|
297
380
|
* @description Converts a caller-supplied fields map into the raw values
|
|
298
381
|
* expected by Keystone's RecordWrite body. If a value is already a raw value
|
package/src/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SFA_KIT_VERSION = "1.
|
|
1
|
+
export declare const SFA_KIT_VERSION = "1.9.0";
|
package/src/lib/version.js
CHANGED