@open-mercato/shared 0.6.7-develop.6669.1.40b669666b → 0.6.7-develop.6670.1.4efa7961c6

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.
@@ -4,6 +4,7 @@ let currentScope = {
4
4
  tenantId: null
5
5
  };
6
6
  let currentVersion = 0;
7
+ let hasEmitted = false;
7
8
  function getCurrentOrganizationScope() {
8
9
  return { ...currentScope };
9
10
  }
@@ -13,11 +14,15 @@ function getCurrentOrganizationScopeVersion() {
13
14
  function emitOrganizationScopeChanged(detail) {
14
15
  if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
15
16
  const hasChanged = currentScope.organizationId !== detail.organizationId || currentScope.tenantId !== detail.tenantId;
17
+ const isFirstEmit = !hasEmitted;
16
18
  currentScope = { ...detail };
19
+ hasEmitted = true;
17
20
  if (hasChanged) {
18
21
  currentVersion++;
19
22
  }
20
- window.dispatchEvent(new CustomEvent(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }));
23
+ if (hasChanged || isFirstEmit) {
24
+ window.dispatchEvent(new CustomEvent(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }));
25
+ }
21
26
  }
22
27
  function subscribeOrganizationScopeChanged(handler) {
23
28
  if (typeof window === "undefined") return () => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/frontend/organizationEvents.ts"],
4
- "sourcesContent": ["export const ORGANIZATION_SCOPE_CHANGED_EVENT = 'om:organization-scope-changed'\n\nexport type OrganizationScopeChangedDetail = {\n organizationId: string | null\n tenantId: string | null\n}\n\n// Module-level state to track current scope and version\nlet currentScope: OrganizationScopeChangedDetail = { \n organizationId: null, \n tenantId: null \n}\nlet currentVersion = 0\n\nexport function getCurrentOrganizationScope(): OrganizationScopeChangedDetail {\n return { ...currentScope }\n}\n\nexport function getCurrentOrganizationScopeVersion(): number {\n return currentVersion\n}\n\nexport function emitOrganizationScopeChanged(detail: OrganizationScopeChangedDetail): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n \n // Detect actual changes\n const hasChanged = \n currentScope.organizationId !== detail.organizationId ||\n currentScope.tenantId !== detail.tenantId\n \n // Update module-level state\n currentScope = { ...detail }\n \n // Increment version only if actual change detected\n if (hasChanged) {\n currentVersion++\n }\n \n // Emit event\n window.dispatchEvent(new CustomEvent<OrganizationScopeChangedDetail>(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }))\n}\n\nexport function subscribeOrganizationScopeChanged(\n handler: (detail: OrganizationScopeChangedDetail) => void\n): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<OrganizationScopeChangedDetail>).detail ?? { organizationId: null, tenantId: null }\n handler(detail)\n }\n window.addEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)\n return () => {\n window.removeEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)\n }\n}\n"],
5
- "mappings": "AAAO,MAAM,mCAAmC;AAQhD,IAAI,eAA+C;AAAA,EACjD,gBAAgB;AAAA,EAChB,UAAU;AACZ;AACA,IAAI,iBAAiB;AAEd,SAAS,8BAA8D;AAC5E,SAAO,EAAE,GAAG,aAAa;AAC3B;AAEO,SAAS,qCAA6C;AAC3D,SAAO;AACT;AAEO,SAAS,6BAA6B,QAA8C;AACzF,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AAGzE,QAAM,aACJ,aAAa,mBAAmB,OAAO,kBACvC,aAAa,aAAa,OAAO;AAGnC,iBAAe,EAAE,GAAG,OAAO;AAG3B,MAAI,YAAY;AACd;AAAA,EACF;AAGA,SAAO,cAAc,IAAI,YAA4C,kCAAkC,EAAE,OAAO,CAAC,CAAC;AACpH;AAEO,SAAS,kCACd,SACY;AACZ,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB;AACjC,UAAM,SAAU,MAAsD,UAAU,EAAE,gBAAgB,MAAM,UAAU,KAAK;AACvH,YAAQ,MAAM;AAAA,EAChB;AACA,SAAO,iBAAiB,kCAAkC,QAAyB;AACnF,SAAO,MAAM;AACX,WAAO,oBAAoB,kCAAkC,QAAyB;AAAA,EACxF;AACF;",
4
+ "sourcesContent": ["export const ORGANIZATION_SCOPE_CHANGED_EVENT = 'om:organization-scope-changed'\n\nexport type OrganizationScopeChangedDetail = {\n organizationId: string | null\n tenantId: string | null\n}\n\n// Module-level state to track current scope and version\nlet currentScope: OrganizationScopeChangedDetail = { \n organizationId: null, \n tenantId: null \n}\nlet currentVersion = 0\nlet hasEmitted = false\n\nexport function getCurrentOrganizationScope(): OrganizationScopeChangedDetail {\n return { ...currentScope }\n}\n\nexport function getCurrentOrganizationScopeVersion(): number {\n return currentVersion\n}\n\nexport function emitOrganizationScopeChanged(detail: OrganizationScopeChangedDetail): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n \n // Detect actual changes\n const hasChanged = \n currentScope.organizationId !== detail.organizationId ||\n currentScope.tenantId !== detail.tenantId\n // Guard so the very first emission always syncs subscribers, even when the\n // initial scope matches the default all-orgs `{ null, null }` state.\n const isFirstEmit = !hasEmitted\n \n // Update module-level state\n currentScope = { ...detail }\n hasEmitted = true\n \n // Increment version only if actual change detected\n if (hasChanged) {\n currentVersion++\n }\n \n // Only dispatch on real scope changes (or the first emission). Suppressing\n // no-op re-emits avoids spurious DataTable refreshes, chrome refetches, and\n // scope-hook churn when navigating between pages with the same scope.\n if (hasChanged || isFirstEmit) {\n window.dispatchEvent(new CustomEvent<OrganizationScopeChangedDetail>(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }))\n }\n}\n\nexport function subscribeOrganizationScopeChanged(\n handler: (detail: OrganizationScopeChangedDetail) => void\n): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => {\n const detail = (event as CustomEvent<OrganizationScopeChangedDetail>).detail ?? { organizationId: null, tenantId: null }\n handler(detail)\n }\n window.addEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)\n return () => {\n window.removeEventListener(ORGANIZATION_SCOPE_CHANGED_EVENT, listener as EventListener)\n }\n}\n"],
5
+ "mappings": "AAAO,MAAM,mCAAmC;AAQhD,IAAI,eAA+C;AAAA,EACjD,gBAAgB;AAAA,EAChB,UAAU;AACZ;AACA,IAAI,iBAAiB;AACrB,IAAI,aAAa;AAEV,SAAS,8BAA8D;AAC5E,SAAO,EAAE,GAAG,aAAa;AAC3B;AAEO,SAAS,qCAA6C;AAC3D,SAAO;AACT;AAEO,SAAS,6BAA6B,QAA8C;AACzF,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AAGzE,QAAM,aACJ,aAAa,mBAAmB,OAAO,kBACvC,aAAa,aAAa,OAAO;AAGnC,QAAM,cAAc,CAAC;AAGrB,iBAAe,EAAE,GAAG,OAAO;AAC3B,eAAa;AAGb,MAAI,YAAY;AACd;AAAA,EACF;AAKA,MAAI,cAAc,aAAa;AAC7B,WAAO,cAAc,IAAI,YAA4C,kCAAkC,EAAE,OAAO,CAAC,CAAC;AAAA,EACpH;AACF;AAEO,SAAS,kCACd,SACY;AACZ,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB;AACjC,UAAM,SAAU,MAAsD,UAAU,EAAE,gBAAgB,MAAM,UAAU,KAAK;AACvH,YAAQ,MAAM;AAAA,EAChB;AACA,SAAO,iBAAiB,kCAAkC,QAAyB;AACnF,SAAO,MAAM;AACX,WAAO,oBAAoB,kCAAkC,QAAyB;AAAA,EACxF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.7-develop.6669.1.40b669666b";
1
+ const APP_VERSION = "0.6.7-develop.6670.1.4efa7961c6";
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.7-develop.6669.1.40b669666b'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6670.1.4efa7961c6'\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.7-develop.6669.1.40b669666b",
3
+ "version": "0.6.7-develop.6670.1.4efa7961c6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@mikro-orm/core": "^7.1.5",
98
98
  "@mikro-orm/decorators": "^7.1.5",
99
99
  "@mikro-orm/postgresql": "^7.1.5",
100
- "@open-mercato/cache": "0.6.7-develop.6669.1.40b669666b",
100
+ "@open-mercato/cache": "0.6.7-develop.6670.1.4efa7961c6",
101
101
  "@types/sanitize-html": "^2.16.1",
102
102
  "dotenv": "^17.4.2",
103
103
  "pino": "^10.3.1",
@@ -26,6 +26,7 @@ describe('CommandBus cache invalidation for sales documents', () => {
26
26
  afterEach(() => {
27
27
  unregisterCommand('sales.orders.update')
28
28
  unregisterCommand('sales.orders.noop-update')
29
+ unregisterCommand('wms.warehouses.create')
29
30
  invalidateMock.mockClear()
30
31
  })
31
32
 
@@ -194,4 +195,49 @@ describe('CommandBus cache invalidation for sales documents', () => {
194
195
  expect.any(Array)
195
196
  )
196
197
  })
198
+
199
+ it('resolves record id from warehouseId-style command results for cache tags', async () => {
200
+ const logMock = jest.fn(async () => ({ id: 'log-entry' }))
201
+
202
+ registerCommand({
203
+ id: 'wms.warehouses.create',
204
+ execute: jest.fn(async () => ({ warehouseId: 'warehouse-uuid-1' })),
205
+ buildLog: jest.fn(() => ({
206
+ actionLabel: 'Create warehouse',
207
+ resourceKind: 'wms.warehouse',
208
+ tenantId: 'tenant-1',
209
+ organizationId: 'org-1',
210
+ })),
211
+ })
212
+
213
+ const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
214
+ container.register({
215
+ actionLogService: asValue({
216
+ log: logMock,
217
+ findByUndoToken: jest.fn(async () => null),
218
+ markUndone: jest.fn(async () => {}),
219
+ }),
220
+ dataEngine: asValue({ flushOrmEntityChanges: jest.fn() }),
221
+ })
222
+
223
+ const bus = new CommandBus()
224
+ const ctx = {
225
+ container,
226
+ auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' },
227
+ organizationScope: null,
228
+ selectedOrganizationId: 'org-1',
229
+ organizationIds: null,
230
+ }
231
+
232
+ await bus.execute('wms.warehouses.create', { input: {}, ctx })
233
+
234
+ expect(invalidateMock).toHaveBeenCalledWith(
235
+ container,
236
+ 'wms.warehouse',
237
+ { id: 'warehouse-uuid-1', organizationId: 'org-1', tenantId: 'tenant-1' },
238
+ 'tenant-1',
239
+ 'command:wms.warehouses.create:execute',
240
+ expect.any(Array)
241
+ )
242
+ })
197
243
  })
@@ -48,6 +48,28 @@ function asRecord(input: unknown): Record<string, unknown> | null {
48
48
  return input as Record<string, unknown>
49
49
  }
50
50
 
51
+ /** Command handlers often return domain keys (e.g. warehouseId) without `id`; cache invalidation must still resolve the record. */
52
+ function extractPrimaryIdFromCommandResult(result: unknown): string | null {
53
+ const r = asRecord(result)
54
+ if (!r) return null
55
+ const direct = pickFirstIdentifier(r.id, r.entityId, r.recordId)
56
+ if (direct) return direct
57
+ for (const key of [
58
+ 'warehouseId',
59
+ 'zoneId',
60
+ 'locationId',
61
+ 'lotId',
62
+ 'reservationId',
63
+ 'profileId',
64
+ 'movementId',
65
+ 'balanceId',
66
+ ]) {
67
+ const v = r[key]
68
+ if (typeof v === 'string' && v.trim().length > 0) return v.trim()
69
+ }
70
+ return null
71
+ }
72
+
51
73
  function toISOString(value: unknown): string | null {
52
74
  if (value instanceof Date) {
53
75
  const iso = value.toISOString()
@@ -584,6 +606,7 @@ export class CommandBus {
584
606
 
585
607
  const recordId = pickFirstIdentifier(
586
608
  metadata?.resourceId,
609
+ extractPrimaryIdFromCommandResult(result),
587
610
  resultRecord?.entityId,
588
611
  resultRecord?.id,
589
612
  resultRecord?.recordId,
@@ -228,6 +228,8 @@ export type ListConfig<TList> = {
228
228
  * filters/sorts, `search_tokens` fulltext filtering, and vector-search branches are bypassed.
229
229
  */
230
230
  omitAutomaticTenantOrgScope?: boolean
231
+ /** When true, skip server-side CRUD GET cache for this list (avoids stale empty payloads after mutations). */
232
+ disableListCache?: boolean
231
233
  }
232
234
 
233
235
  export type CrudExportColumnConfig = {
@@ -1461,7 +1463,8 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
1461
1463
  const exportFullRequested = exportRequested && (exportScope === 'full' || parseBooleanToken((queryParams as any).full) === true)
1462
1464
  profiler.mark('export_configured', { exportRequested, exportFullRequested })
1463
1465
 
1464
- const cacheEnabled = isCrudCacheEnabled() && !exportRequested
1466
+ const cacheEnabled =
1467
+ isCrudCacheEnabled() && !exportRequested && !opts.list?.disableListCache
1465
1468
  const cacheTimerStart = cacheEnabled && isCrudCacheDebugEnabled()
1466
1469
  ? process.hrtime.bigint()
1467
1470
  : null
@@ -11,6 +11,7 @@ let currentScope: OrganizationScopeChangedDetail = {
11
11
  tenantId: null
12
12
  }
13
13
  let currentVersion = 0
14
+ let hasEmitted = false
14
15
 
15
16
  export function getCurrentOrganizationScope(): OrganizationScopeChangedDetail {
16
17
  return { ...currentScope }
@@ -27,17 +28,25 @@ export function emitOrganizationScopeChanged(detail: OrganizationScopeChangedDet
27
28
  const hasChanged =
28
29
  currentScope.organizationId !== detail.organizationId ||
29
30
  currentScope.tenantId !== detail.tenantId
31
+ // Guard so the very first emission always syncs subscribers, even when the
32
+ // initial scope matches the default all-orgs `{ null, null }` state.
33
+ const isFirstEmit = !hasEmitted
30
34
 
31
35
  // Update module-level state
32
36
  currentScope = { ...detail }
37
+ hasEmitted = true
33
38
 
34
39
  // Increment version only if actual change detected
35
40
  if (hasChanged) {
36
41
  currentVersion++
37
42
  }
38
43
 
39
- // Emit event
40
- window.dispatchEvent(new CustomEvent<OrganizationScopeChangedDetail>(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }))
44
+ // Only dispatch on real scope changes (or the first emission). Suppressing
45
+ // no-op re-emits avoids spurious DataTable refreshes, chrome refetches, and
46
+ // scope-hook churn when navigating between pages with the same scope.
47
+ if (hasChanged || isFirstEmit) {
48
+ window.dispatchEvent(new CustomEvent<OrganizationScopeChangedDetail>(ORGANIZATION_SCOPE_CHANGED_EVENT, { detail }))
49
+ }
41
50
  }
42
51
 
43
52
  export function subscribeOrganizationScopeChanged(
@@ -248,6 +248,13 @@ export type InjectionWidgetModule<TContext = unknown, TData = unknown> = {
248
248
 
249
249
  export type InjectionColumnDefinition = {
250
250
  id: string
251
+ /**
252
+ * i18n translation key for the column header.
253
+ * When present, consumers should call `t(headerKey, header)` instead of treating `header` as a key.
254
+ * Follows the same `label`/`labelKey` convention used by `InjectionFieldDefinition`.
255
+ */
256
+ headerKey?: string
257
+ /** Display text (and i18n fallback) for the column header. */
251
258
  header: string
252
259
  accessorKey: string
253
260
  cell?: (props: { getValue: () => unknown }) => ReactNode