@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "React hooks and components for building pages that connect to GENIEE SFA/CRM data.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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 raw = yield this.fetchGet(this.objectsPath(`/${enc(objectKey)}`));
105
- return { object: normalizeObjectDetail(raw) };
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(fields) }, extra);
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(fields) }, { 'If-Match': version });
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
@@ -1 +1 @@
1
- export declare const SFA_KIT_VERSION = "1.8.0";
1
+ export declare const SFA_KIT_VERSION = "1.9.0";
@@ -1,3 +1,3 @@
1
1
  // このファイルは libs/sfa-kit/scripts/sync-version.cjs が生成する。
2
2
  // 手で編集しないこと。バージョンを上げるときは libs/sfa-kit/package.json を変更する。
3
- export const SFA_KIT_VERSION = '1.8.0';
3
+ export const SFA_KIT_VERSION = '1.9.0';