@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.
- package/dist/lib/commands/command-bus.js +21 -0
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/crud/factory.js +1 -1
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/frontend/organizationEvents.js +6 -1
- package/dist/lib/frontend/organizationEvents.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/commands/__tests__/command-bus.cache.test.ts +46 -0
- package/src/lib/commands/command-bus.ts +23 -0
- package/src/lib/crud/factory.ts +4 -1
- package/src/lib/frontend/organizationEvents.ts +11 -2
- package/src/modules/widgets/injection.ts +7 -0
|
@@ -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
|
-
|
|
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 //
|
|
5
|
-
"mappings": "AAAO,MAAM,mCAAmC;AAQhD,IAAI,eAA+C;AAAA,EACjD,gBAAgB;AAAA,EAChB,UAAU;AACZ;AACA,IAAI,iBAAiB;
|
|
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
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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,
|
package/src/lib/crud/factory.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
40
|
-
|
|
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
|