@open-mercato/shared 0.6.1-develop.3090.06ab462170 → 0.6.1-develop.3102.d6e7e6d57a

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.
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.1-develop.3090.06ab462170";
1
+ const APP_VERSION = "0.6.1-develop.3102.d6e7e6d57a";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.3090.06ab462170'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.3102.d6e7e6d57a'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.1-develop.3090.06ab462170",
3
+ "version": "0.6.1-develop.3102.d6e7e6d57a",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -89,10 +89,10 @@
89
89
  }
90
90
  },
91
91
  "dependencies": {
92
- "@mikro-orm/core": "^7.0.13",
93
- "@mikro-orm/decorators": "^7.0.13",
94
- "@mikro-orm/postgresql": "^7.0.13",
95
- "@open-mercato/cache": "0.6.1-develop.3090.06ab462170",
92
+ "@mikro-orm/core": "^7.0.14",
93
+ "@mikro-orm/decorators": "^7.0.14",
94
+ "@mikro-orm/postgresql": "^7.0.14",
95
+ "@open-mercato/cache": "0.6.1-develop.3102.d6e7e6d57a",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.0.1",
98
98
  "reflect-metadata": "^0.2.2",
@@ -1,4 +1,5 @@
1
1
  import {
2
+ applyCustomFieldsNormalization,
2
3
  buildCustomFieldFiltersFromQuery,
3
4
  decorateRecordWithCustomFields,
4
5
  extractAllCustomFieldEntries,
@@ -298,6 +299,62 @@ describe('decorateRecordWithCustomFields', () => {
298
299
  })
299
300
  })
300
301
 
302
+ describe('applyCustomFieldsNormalization', () => {
303
+ const definitionIndex: CustomFieldDefinitionIndex = new Map([
304
+ [
305
+ 'priority',
306
+ [
307
+ {
308
+ key: 'priority',
309
+ label: 'Priority',
310
+ kind: 'integer',
311
+ multi: false,
312
+ organizationId: null,
313
+ tenantId: null,
314
+ priority: 0,
315
+ updatedAt: 1,
316
+ },
317
+ ],
318
+ ],
319
+ ])
320
+
321
+ it('preserves cf_* keys by default for backward compatibility', () => {
322
+ const record = { id: 'r-1', name: 'Item', cf_priority: 5, 'cf:priority': 5 }
323
+ const decorated = decorateRecordWithCustomFields(record, definitionIndex, {})
324
+ const result = applyCustomFieldsNormalization(record, decorated)
325
+
326
+ expect(result.id).toBe('r-1')
327
+ expect(result.cf_priority).toBe(5)
328
+ expect(result['cf:priority']).toBe(5)
329
+ expect(result.customValues).toEqual({ priority: 5 })
330
+ expect(Array.isArray(result.customFields)).toBe(true)
331
+ expect((result.customFields as any[])[0]).toMatchObject({ key: 'priority', value: 5 })
332
+ })
333
+
334
+ it('strips cf_* and cf:* keys when stripPrefixedKeys is enabled (issue #1769)', () => {
335
+ const record = { id: 'r-1', name: 'Item', cf_priority: 5, 'cf:priority': 5 }
336
+ const decorated = decorateRecordWithCustomFields(record, definitionIndex, {})
337
+ const result = applyCustomFieldsNormalization(record, decorated, { stripPrefixedKeys: true })
338
+
339
+ expect(result.id).toBe('r-1')
340
+ expect(result.name).toBe('Item')
341
+ expect('cf_priority' in result).toBe(false)
342
+ expect('cf:priority' in result).toBe(false)
343
+ expect(result.customValues).toEqual({ priority: 5 })
344
+ expect(Array.isArray(result.customFields)).toBe(true)
345
+ })
346
+
347
+ it('emits null customValues when no active definitions match', () => {
348
+ const record = { id: 'r-1', cf_unknown: 'leftover' }
349
+ const decorated = decorateRecordWithCustomFields(record, new Map(), {})
350
+ const result = applyCustomFieldsNormalization(record, decorated, { stripPrefixedKeys: true })
351
+
352
+ expect(result.customValues).toBeNull()
353
+ expect(result.customFields).toEqual([])
354
+ expect('cf_unknown' in result).toBe(false)
355
+ })
356
+ })
357
+
301
358
  describe('loadCustomFieldDefinitionIndex', () => {
302
359
  it('filters definition summaries by selected fieldset membership', async () => {
303
360
  const em = mockEntityManager([
@@ -406,6 +406,32 @@ export async function loadCustomFieldDefinitionIndex(opts: {
406
406
  return index
407
407
  }
408
408
 
409
+ export type ApplyCustomFieldsNormalizationOptions = {
410
+ /**
411
+ * When true, removes raw `cf_*` and `cf:*` keys from the record once they
412
+ * have been extracted into `customValues` / `customFields`. Produces a single
413
+ * canonical response shape (issue #1769). Defaults to `false` to preserve the
414
+ * existing wire format for callers that read `cf_*` from the top level.
415
+ */
416
+ stripPrefixedKeys?: boolean
417
+ }
418
+
419
+ export function applyCustomFieldsNormalization(
420
+ record: Record<string, unknown>,
421
+ decorated: CustomFieldDisplayPayload,
422
+ options: ApplyCustomFieldsNormalizationOptions = {},
423
+ ): Record<string, unknown> {
424
+ const stripPrefixedKeys = options.stripPrefixedKeys === true
425
+ const base: Record<string, unknown> = {}
426
+ for (const [key, value] of Object.entries(record)) {
427
+ if (stripPrefixedKeys && (key.startsWith('cf_') || key.startsWith('cf:'))) continue
428
+ base[key] = value
429
+ }
430
+ base.customValues = decorated.customValues
431
+ base.customFields = decorated.customFields
432
+ return base
433
+ }
434
+
409
435
  export function decorateRecordWithCustomFields(
410
436
  record: Record<string, unknown>,
411
437
  definitions: CustomFieldDefinitionIndex,
@@ -30,6 +30,7 @@ import {
30
30
  extractCustomFieldValuesFromPayload,
31
31
  extractAllCustomFieldEntries,
32
32
  decorateRecordWithCustomFields,
33
+ applyCustomFieldsNormalization,
33
34
  loadCustomFieldDefinitionIndex,
34
35
  } from './custom-fields'
35
36
  import { serializeExport, normalizeExportFormat, defaultExportFilename, ensureColumns, type CrudExportFormat, type PreparedExport } from './exporters'
@@ -162,6 +163,14 @@ export type CustomFieldsConfig =
162
163
  export type CrudListCustomFieldDecorator = {
163
164
  entityIds: EntityId | EntityId[]
164
165
  resolveContext?: (item: any, ctx: CrudCtx) => { organizationId?: string | null; tenantId?: string | null }
166
+ /**
167
+ * When true, the factory removes raw `cf_*` and `cf:*` keys from each list
168
+ * item after extracting them into `customValues` / `customFields`. Recommended
169
+ * for new modules — produces the single canonical response shape requested in
170
+ * #1769. Defaults to `false` so existing callers that read `cf_*` from the
171
+ * top level keep working until they migrate.
172
+ */
173
+ stripPrefixedKeys?: boolean
165
174
  }
166
175
 
167
176
  export type ListConfig<TList> = {
@@ -924,12 +933,9 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
924
933
  organizationId: organizationId ?? null,
925
934
  tenantId: tenantId ?? null,
926
935
  })
927
- const output = {
928
- ...item,
929
- customValues: decorated.customValues,
930
- customFields: decorated.customFields,
931
- }
932
- return output
936
+ return applyCustomFieldsNormalization(item, decorated, {
937
+ stripPrefixedKeys: listCustomFieldDecorator.stripPrefixedKeys === true,
938
+ })
933
939
  })
934
940
  cfProfiler.mark('decorate_complete', { itemCount: decoratedItems.length })
935
941
  endProfile({