@open-mercato/shared 0.6.3-develop.3805.1.357827108c → 0.6.3-develop.3810.1.ad92c339f5

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.
@@ -85,8 +85,21 @@ function pickFirstIdentifier(...values) {
85
85
  }
86
86
  return null;
87
87
  }
88
+ const IRREGULAR_PLURALS = {
89
+ people: "person",
90
+ children: "child",
91
+ mice: "mouse",
92
+ men: "man",
93
+ women: "woman",
94
+ geese: "goose",
95
+ feet: "foot",
96
+ teeth: "tooth",
97
+ oxen: "ox"
98
+ };
88
99
  function singularizeSegment(segment) {
89
100
  const lower = segment.toLowerCase();
101
+ const irregular = IRREGULAR_PLURALS[lower];
102
+ if (irregular) return irregular;
90
103
  if (lower.endsWith("ies") && lower.length > 3) return lower.slice(0, -3) + "y";
91
104
  if (lower.endsWith("ses") && lower.length > 3) return lower.slice(0, -2);
92
105
  if ((lower.endsWith("xes") || lower.endsWith("zes") || lower.endsWith("ches") || lower.endsWith("shes")) && lower.length > 3) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/cache.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseBooleanToken } from '../boolean'\n\nexport type CrudCacheIdentifiers = {\n id?: string | null\n organizationId?: string | null\n tenantId?: string | null\n}\n\nlet crudCacheEnabledFlag: boolean | null = null\nexport function isCrudCacheEnabled(): boolean {\n if (crudCacheEnabledFlag !== null) return crudCacheEnabledFlag\n crudCacheEnabledFlag = parseBooleanToken(process.env.ENABLE_CRUD_API_CACHE ?? '') === true\n return crudCacheEnabledFlag\n}\n\nlet crudCacheDebugFlag: boolean | null = null\nexport function isCrudCacheDebugEnabled(): boolean {\n if (crudCacheDebugFlag !== null) return crudCacheDebugFlag\n crudCacheDebugFlag = parseBooleanToken(process.env.QUERY_ENGINE_DEBUG_SQL ?? '') === true\n return crudCacheDebugFlag\n}\n\nexport function debugCrudCache(event: string, context: Record<string, unknown>) {\n if (!isCrudCacheDebugEnabled()) return\n try {\n console.debug('[crud][cache]', event, context)\n } catch {}\n}\n\nexport function resolveCrudCache(container: AwilixContainer): CacheStrategy | null {\n try {\n const cache = (container.resolve('cache') as CacheStrategy)\n if (cache && typeof cache.get === 'function' && typeof cache.set === 'function') {\n return cache\n }\n } catch {}\n return null\n}\n\nexport function normalizeTagSegment(value: string | null | undefined): string {\n if (value === null || value === undefined || value === '') return 'null'\n return value.toString().trim().replace(/[^a-zA-Z0-9._-]/g, '-')\n}\n\nexport function canonicalizeResourceTag(value: string | null | undefined): string | null {\n if (value === null || value === undefined) return null\n const trimmed = String(value).trim()\n if (!trimmed.length) return null\n const withSeparators = trimmed\n .replace(/::/g, '.')\n .replace(/[/\\\\]+/g, '.')\n .replace(/[\\s]+/g, '.')\n .replace(/_/g, '.')\n .replace(/-+/g, '.')\n const withCamelBreaks = withSeparators.replace(/([a-z0-9])([A-Z])/g, '$1.$2')\n const collapsed = withCamelBreaks.replace(/\\.{2,}/g, '.').replace(/(?:^\\.+|\\.+$)/g, '')\n const lowered = collapsed.toLowerCase()\n return lowered.length ? lowered : null\n}\n\nexport function buildRecordTag(resource: string, tenantId: string | null, recordId: string): string {\n return [\n 'crud',\n normalizeTagSegment(resource),\n 'tenant',\n normalizeTagSegment(tenantId),\n 'record',\n normalizeTagSegment(recordId),\n ].join(':')\n}\n\nexport function buildCollectionTags(\n resource: string,\n tenantId: string | null,\n organizationIds: Array<string | null>\n): string[] {\n const normalizedResource = normalizeTagSegment(resource)\n const normalizedTenant = normalizeTagSegment(tenantId)\n const tags = new Set<string>()\n if (!organizationIds.length) {\n tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', 'null', 'collection'].join(':'))\n return Array.from(tags)\n }\n for (const orgId of organizationIds) {\n tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', normalizeTagSegment(orgId), 'collection'].join(':'))\n }\n return Array.from(tags)\n}\n\nexport function normalizeIdentifierValue(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'bigint') return String(value)\n if (typeof value === 'object') {\n if (value instanceof Date) return value.toISOString()\n if (value && typeof (value as { id?: unknown }).id !== 'undefined') {\n return normalizeIdentifierValue((value as { id?: unknown }).id)\n }\n }\n return String(value)\n}\n\nexport function pickFirstIdentifier(...values: Array<unknown>): string | null {\n for (const value of values) {\n const normalized = normalizeIdentifierValue(value)\n if (normalized) return normalized\n }\n return null\n}\n\nfunction singularizeSegment(segment: string): string {\n const lower = segment.toLowerCase()\n if (lower.endsWith('ies') && lower.length > 3) return lower.slice(0, -3) + 'y'\n if (lower.endsWith('ses') && lower.length > 3) return lower.slice(0, -2)\n if (\n (lower.endsWith('xes') ||\n lower.endsWith('zes') ||\n lower.endsWith('ches') ||\n lower.endsWith('shes')) &&\n lower.length > 3\n ) {\n return lower.slice(0, -2)\n }\n if (lower.endsWith('s') && !lower.endsWith('ss') && lower.length > 1) return lower.slice(0, -1)\n return lower\n}\n\nfunction singularizeResourceSegment(segment: string): string {\n return segment\n .split('-')\n .map((part) => singularizeSegment(part))\n .join('-')\n}\n\nexport function deriveResourceFromCommandId(commandId: string | undefined | null): string | null {\n if (!commandId || typeof commandId !== 'string') return null\n const parts = commandId.split('.')\n if (parts.length < 2) return null\n const modulePart = parts[0]\n const entityPart = singularizeResourceSegment(parts[1])\n if (!modulePart || !entityPart) return null\n return `${modulePart}.${entityPart}`\n}\n\nexport function expandResourceAliases(resource: string, aliases?: string[]): string[] {\n const set = new Set<string>()\n const inputs = [resource, ...(aliases ?? [])]\n for (const candidate of inputs) {\n const canonical = canonicalizeResourceTag(candidate)\n if (canonical) set.add(canonical)\n }\n if (!set.size) return []\n return Array.from(set)\n}\n\nexport async function invalidateCrudCache(\n container: AwilixContainer,\n resource: string,\n identifiers: CrudCacheIdentifiers,\n fallbackTenant: string | null,\n reason: string,\n aliases?: string[]\n): Promise<void> {\n if (!isCrudCacheEnabled()) return\n const cache = resolveCrudCache(container)\n if (!cache || typeof cache.deleteByTags !== 'function') return\n const resources = expandResourceAliases(resource, aliases)\n const tenantId = identifiers.tenantId ?? fallbackTenant ?? null\n const recordId = normalizeIdentifierValue(identifiers.id)\n const tags = new Set<string>()\n for (const key of resources) {\n if (recordId) {\n tags.add(buildRecordTag(key, tenantId, recordId))\n }\n const organizationIds: Array<string | null> = []\n if (identifiers.organizationId !== undefined) {\n organizationIds.push(identifiers.organizationId ?? null)\n }\n if (!organizationIds.length) organizationIds.push(null)\n for (const tag of buildCollectionTags(key, tenantId, organizationIds)) {\n tags.add(tag)\n }\n }\n if (!tags.size) return\n const tagList = Array.from(tags)\n debugCrudCache('invalidate', {\n resource,\n aliases: resources,\n reason,\n tenantId: tenantId ?? 'null',\n tags: tagList,\n action: 'clearing',\n })\n const deleted = await runWithCacheTenant(tenantId, () => cache.deleteByTags(tagList))\n debugCrudCache('invalidate', {\n resource,\n reason,\n tenantId: tenantId ?? 'null',\n tags: tagList,\n action: 'cleared',\n deleted,\n })\n}\n"],
5
- "mappings": "AACA,SAAS,0BAA8C;AACvD,SAAS,yBAAyB;AAQlC,IAAI,uBAAuC;AACpC,SAAS,qBAA8B;AAC5C,MAAI,yBAAyB,KAAM,QAAO;AAC1C,yBAAuB,kBAAkB,QAAQ,IAAI,yBAAyB,EAAE,MAAM;AACtF,SAAO;AACT;AAEA,IAAI,qBAAqC;AAClC,SAAS,0BAAmC;AACjD,MAAI,uBAAuB,KAAM,QAAO;AACxC,uBAAqB,kBAAkB,QAAQ,IAAI,0BAA0B,EAAE,MAAM;AACrF,SAAO;AACT;AAEO,SAAS,eAAe,OAAe,SAAkC;AAC9E,MAAI,CAAC,wBAAwB,EAAG;AAChC,MAAI;AACF,YAAQ,MAAM,iBAAiB,OAAO,OAAO;AAAA,EAC/C,QAAQ;AAAA,EAAC;AACX;AAEO,SAAS,iBAAiB,WAAkD;AACjF,MAAI;AACF,UAAM,QAAS,UAAU,QAAQ,OAAO;AACxC,QAAI,SAAS,OAAO,MAAM,QAAQ,cAAc,OAAO,MAAM,QAAQ,YAAY;AAC/E,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;AAEO,SAAS,oBAAoB,OAA0C;AAC5E,MAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GAAI,QAAO;AAClE,SAAO,MAAM,SAAS,EAAE,KAAK,EAAE,QAAQ,oBAAoB,GAAG;AAChE;AAEO,SAAS,wBAAwB,OAAiD;AACvF,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,UAAU,OAAO,KAAK,EAAE,KAAK;AACnC,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,QAAM,iBAAiB,QACpB,QAAQ,OAAO,GAAG,EAClB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,MAAM,GAAG,EACjB,QAAQ,OAAO,GAAG;AACrB,QAAM,kBAAkB,eAAe,QAAQ,sBAAsB,OAAO;AAC5E,QAAM,YAAY,gBAAgB,QAAQ,WAAW,GAAG,EAAE,QAAQ,kBAAkB,EAAE;AACtF,QAAM,UAAU,UAAU,YAAY;AACtC,SAAO,QAAQ,SAAS,UAAU;AACpC;AAEO,SAAS,eAAe,UAAkB,UAAyB,UAA0B;AAClG,SAAO;AAAA,IACL;AAAA,IACA,oBAAoB,QAAQ;AAAA,IAC5B;AAAA,IACA,oBAAoB,QAAQ;AAAA,IAC5B;AAAA,IACA,oBAAoB,QAAQ;AAAA,EAC9B,EAAE,KAAK,GAAG;AACZ;AAEO,SAAS,oBACd,UACA,UACA,iBACU;AACV,QAAM,qBAAqB,oBAAoB,QAAQ;AACvD,QAAM,mBAAmB,oBAAoB,QAAQ;AACrD,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,CAAC,gBAAgB,QAAQ;AAC3B,SAAK,IAAI,CAAC,QAAQ,oBAAoB,UAAU,kBAAkB,OAAO,QAAQ,YAAY,EAAE,KAAK,GAAG,CAAC;AACxG,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACA,aAAW,SAAS,iBAAiB;AACnC,SAAK,IAAI,CAAC,QAAQ,oBAAoB,UAAU,kBAAkB,OAAO,oBAAoB,KAAK,GAAG,YAAY,EAAE,KAAK,GAAG,CAAC;AAAA,EAC9H;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,yBAAyB,OAA+B;AACtE,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK;AAC/E,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,QAAI,SAAS,OAAQ,MAA2B,OAAO,aAAa;AAClE,aAAO,yBAA0B,MAA2B,EAAE;AAAA,IAChE;AAAA,EACF;AACA,SAAO,OAAO,KAAK;AACrB;AAEO,SAAS,uBAAuB,QAAuC;AAC5E,aAAW,SAAS,QAAQ;AAC1B,UAAM,aAAa,yBAAyB,KAAK;AACjD,QAAI,WAAY,QAAO;AAAA,EACzB;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAyB;AACnD,QAAM,QAAQ,QAAQ,YAAY;AAClC,MAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,EAAG,QAAO,MAAM,MAAM,GAAG,EAAE,IAAI;AAC3E,MAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,EAAG,QAAO,MAAM,MAAM,GAAG,EAAE;AACvE,OACG,MAAM,SAAS,KAAK,KACnB,MAAM,SAAS,KAAK,KACpB,MAAM,SAAS,MAAM,KACrB,MAAM,SAAS,MAAM,MACvB,MAAM,SAAS,GACf;AACA,WAAO,MAAM,MAAM,GAAG,EAAE;AAAA,EAC1B;AACA,MAAI,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,EAAG,QAAO,MAAM,MAAM,GAAG,EAAE;AAC9F,SAAO;AACT;AAEA,SAAS,2BAA2B,SAAyB;AAC3D,SAAO,QACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,mBAAmB,IAAI,CAAC,EACtC,KAAK,GAAG;AACb;AAEO,SAAS,4BAA4B,WAAqD;AAC/F,MAAI,CAAC,aAAa,OAAO,cAAc,SAAU,QAAO;AACxD,QAAM,QAAQ,UAAU,MAAM,GAAG;AACjC,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,QAAM,aAAa,MAAM,CAAC;AAC1B,QAAM,aAAa,2BAA2B,MAAM,CAAC,CAAC;AACtD,MAAI,CAAC,cAAc,CAAC,WAAY,QAAO;AACvC,SAAO,GAAG,UAAU,IAAI,UAAU;AACpC;AAEO,SAAS,sBAAsB,UAAkB,SAA8B;AACpF,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,SAAS,CAAC,UAAU,GAAI,WAAW,CAAC,CAAE;AAC5C,aAAW,aAAa,QAAQ;AAC9B,UAAM,YAAY,wBAAwB,SAAS;AACnD,QAAI,UAAW,KAAI,IAAI,SAAS;AAAA,EAClC;AACA,MAAI,CAAC,IAAI,KAAM,QAAO,CAAC;AACvB,SAAO,MAAM,KAAK,GAAG;AACvB;AAEA,eAAsB,oBACpB,WACA,UACA,aACA,gBACA,QACA,SACe;AACf,MAAI,CAAC,mBAAmB,EAAG;AAC3B,QAAM,QAAQ,iBAAiB,SAAS;AACxC,MAAI,CAAC,SAAS,OAAO,MAAM,iBAAiB,WAAY;AACxD,QAAM,YAAY,sBAAsB,UAAU,OAAO;AACzD,QAAM,WAAW,YAAY,YAAY,kBAAkB;AAC3D,QAAM,WAAW,yBAAyB,YAAY,EAAE;AACxD,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,OAAO,WAAW;AAC3B,QAAI,UAAU;AACZ,WAAK,IAAI,eAAe,KAAK,UAAU,QAAQ,CAAC;AAAA,IAClD;AACA,UAAM,kBAAwC,CAAC;AAC/C,QAAI,YAAY,mBAAmB,QAAW;AAC5C,sBAAgB,KAAK,YAAY,kBAAkB,IAAI;AAAA,IACzD;AACA,QAAI,CAAC,gBAAgB,OAAQ,iBAAgB,KAAK,IAAI;AACtD,eAAW,OAAO,oBAAoB,KAAK,UAAU,eAAe,GAAG;AACrE,WAAK,IAAI,GAAG;AAAA,IACd;AAAA,EACF;AACA,MAAI,CAAC,KAAK,KAAM;AAChB,QAAM,UAAU,MAAM,KAAK,IAAI;AAC/B,iBAAe,cAAc;AAAA,IAC3B;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,UAAU,YAAY;AAAA,IACtB,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACD,QAAM,UAAU,MAAM,mBAAmB,UAAU,MAAM,MAAM,aAAa,OAAO,CAAC;AACpF,iBAAe,cAAc;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,UAAU,YAAY;AAAA,IACtB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseBooleanToken } from '../boolean'\n\nexport type CrudCacheIdentifiers = {\n id?: string | null\n organizationId?: string | null\n tenantId?: string | null\n}\n\nlet crudCacheEnabledFlag: boolean | null = null\nexport function isCrudCacheEnabled(): boolean {\n if (crudCacheEnabledFlag !== null) return crudCacheEnabledFlag\n crudCacheEnabledFlag = parseBooleanToken(process.env.ENABLE_CRUD_API_CACHE ?? '') === true\n return crudCacheEnabledFlag\n}\n\nlet crudCacheDebugFlag: boolean | null = null\nexport function isCrudCacheDebugEnabled(): boolean {\n if (crudCacheDebugFlag !== null) return crudCacheDebugFlag\n crudCacheDebugFlag = parseBooleanToken(process.env.QUERY_ENGINE_DEBUG_SQL ?? '') === true\n return crudCacheDebugFlag\n}\n\nexport function debugCrudCache(event: string, context: Record<string, unknown>) {\n if (!isCrudCacheDebugEnabled()) return\n try {\n console.debug('[crud][cache]', event, context)\n } catch {}\n}\n\nexport function resolveCrudCache(container: AwilixContainer): CacheStrategy | null {\n try {\n const cache = (container.resolve('cache') as CacheStrategy)\n if (cache && typeof cache.get === 'function' && typeof cache.set === 'function') {\n return cache\n }\n } catch {}\n return null\n}\n\nexport function normalizeTagSegment(value: string | null | undefined): string {\n if (value === null || value === undefined || value === '') return 'null'\n return value.toString().trim().replace(/[^a-zA-Z0-9._-]/g, '-')\n}\n\nexport function canonicalizeResourceTag(value: string | null | undefined): string | null {\n if (value === null || value === undefined) return null\n const trimmed = String(value).trim()\n if (!trimmed.length) return null\n const withSeparators = trimmed\n .replace(/::/g, '.')\n .replace(/[/\\\\]+/g, '.')\n .replace(/[\\s]+/g, '.')\n .replace(/_/g, '.')\n .replace(/-+/g, '.')\n const withCamelBreaks = withSeparators.replace(/([a-z0-9])([A-Z])/g, '$1.$2')\n const collapsed = withCamelBreaks.replace(/\\.{2,}/g, '.').replace(/(?:^\\.+|\\.+$)/g, '')\n const lowered = collapsed.toLowerCase()\n return lowered.length ? lowered : null\n}\n\nexport function buildRecordTag(resource: string, tenantId: string | null, recordId: string): string {\n return [\n 'crud',\n normalizeTagSegment(resource),\n 'tenant',\n normalizeTagSegment(tenantId),\n 'record',\n normalizeTagSegment(recordId),\n ].join(':')\n}\n\nexport function buildCollectionTags(\n resource: string,\n tenantId: string | null,\n organizationIds: Array<string | null>\n): string[] {\n const normalizedResource = normalizeTagSegment(resource)\n const normalizedTenant = normalizeTagSegment(tenantId)\n const tags = new Set<string>()\n if (!organizationIds.length) {\n tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', 'null', 'collection'].join(':'))\n return Array.from(tags)\n }\n for (const orgId of organizationIds) {\n tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', normalizeTagSegment(orgId), 'collection'].join(':'))\n }\n return Array.from(tags)\n}\n\nexport function normalizeIdentifierValue(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'bigint') return String(value)\n if (typeof value === 'object') {\n if (value instanceof Date) return value.toISOString()\n if (value && typeof (value as { id?: unknown }).id !== 'undefined') {\n return normalizeIdentifierValue((value as { id?: unknown }).id)\n }\n }\n return String(value)\n}\n\nexport function pickFirstIdentifier(...values: Array<unknown>): string | null {\n for (const value of values) {\n const normalized = normalizeIdentifierValue(value)\n if (normalized) return normalized\n }\n return null\n}\n\nconst IRREGULAR_PLURALS: Record<string, string> = {\n people: 'person',\n children: 'child',\n mice: 'mouse',\n men: 'man',\n women: 'woman',\n geese: 'goose',\n feet: 'foot',\n teeth: 'tooth',\n oxen: 'ox',\n}\n\nfunction singularizeSegment(segment: string): string {\n const lower = segment.toLowerCase()\n const irregular = IRREGULAR_PLURALS[lower]\n if (irregular) return irregular\n if (lower.endsWith('ies') && lower.length > 3) return lower.slice(0, -3) + 'y'\n if (lower.endsWith('ses') && lower.length > 3) return lower.slice(0, -2)\n if (\n (lower.endsWith('xes') ||\n lower.endsWith('zes') ||\n lower.endsWith('ches') ||\n lower.endsWith('shes')) &&\n lower.length > 3\n ) {\n return lower.slice(0, -2)\n }\n if (lower.endsWith('s') && !lower.endsWith('ss') && lower.length > 1) return lower.slice(0, -1)\n return lower\n}\n\nfunction singularizeResourceSegment(segment: string): string {\n return segment\n .split('-')\n .map((part) => singularizeSegment(part))\n .join('-')\n}\n\nexport function deriveResourceFromCommandId(commandId: string | undefined | null): string | null {\n if (!commandId || typeof commandId !== 'string') return null\n const parts = commandId.split('.')\n if (parts.length < 2) return null\n const modulePart = parts[0]\n const entityPart = singularizeResourceSegment(parts[1])\n if (!modulePart || !entityPart) return null\n return `${modulePart}.${entityPart}`\n}\n\nexport function expandResourceAliases(resource: string, aliases?: string[]): string[] {\n const set = new Set<string>()\n const inputs = [resource, ...(aliases ?? [])]\n for (const candidate of inputs) {\n const canonical = canonicalizeResourceTag(candidate)\n if (canonical) set.add(canonical)\n }\n if (!set.size) return []\n return Array.from(set)\n}\n\nexport async function invalidateCrudCache(\n container: AwilixContainer,\n resource: string,\n identifiers: CrudCacheIdentifiers,\n fallbackTenant: string | null,\n reason: string,\n aliases?: string[]\n): Promise<void> {\n if (!isCrudCacheEnabled()) return\n const cache = resolveCrudCache(container)\n if (!cache || typeof cache.deleteByTags !== 'function') return\n const resources = expandResourceAliases(resource, aliases)\n const tenantId = identifiers.tenantId ?? fallbackTenant ?? null\n const recordId = normalizeIdentifierValue(identifiers.id)\n const tags = new Set<string>()\n for (const key of resources) {\n if (recordId) {\n tags.add(buildRecordTag(key, tenantId, recordId))\n }\n const organizationIds: Array<string | null> = []\n if (identifiers.organizationId !== undefined) {\n organizationIds.push(identifiers.organizationId ?? null)\n }\n if (!organizationIds.length) organizationIds.push(null)\n for (const tag of buildCollectionTags(key, tenantId, organizationIds)) {\n tags.add(tag)\n }\n }\n if (!tags.size) return\n const tagList = Array.from(tags)\n debugCrudCache('invalidate', {\n resource,\n aliases: resources,\n reason,\n tenantId: tenantId ?? 'null',\n tags: tagList,\n action: 'clearing',\n })\n const deleted = await runWithCacheTenant(tenantId, () => cache.deleteByTags(tagList))\n debugCrudCache('invalidate', {\n resource,\n reason,\n tenantId: tenantId ?? 'null',\n tags: tagList,\n action: 'cleared',\n deleted,\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.3-develop.3805.1.357827108c";
1
+ const APP_VERSION = "0.6.3-develop.3810.1.ad92c339f5";
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.3-develop.3805.1.357827108c'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.3-develop.3810.1.ad92c339f5'\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.3-develop.3805.1.357827108c",
3
+ "version": "0.6.3-develop.3810.1.ad92c339f5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.1",
93
93
  "@mikro-orm/decorators": "^7.1.1",
94
94
  "@mikro-orm/postgresql": "^7.1.1",
95
- "@open-mercato/cache": "0.6.3-develop.3805.1.357827108c",
95
+ "@open-mercato/cache": "0.6.3-develop.3810.1.ad92c339f5",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -0,0 +1,64 @@
1
+ jest.mock('@open-mercato/cache', () => ({
2
+ runWithCacheTenant: async <T>(_tenant: string | null, fn: () => Promise<T> | T) => fn(),
3
+ }))
4
+
5
+ import { deriveResourceFromCommandId } from '@open-mercato/shared/lib/crud/cache'
6
+
7
+ describe('deriveResourceFromCommandId — irregular plurals (#2072)', () => {
8
+ it('singularises "people" to "person"', () => {
9
+ // Regression for the resourceKind divergence between hand-written literals
10
+ // ('customers.person') and factory-derived runtime values
11
+ // ('customers.people') — see issue #2072 and PR #2055.
12
+ expect(deriveResourceFromCommandId('customers.people.update')).toBe('customers.person')
13
+ expect(deriveResourceFromCommandId('customers.people.create')).toBe('customers.person')
14
+ expect(deriveResourceFromCommandId('customers.people.delete')).toBe('customers.person')
15
+ })
16
+
17
+ it('handles the other known irregular plurals', () => {
18
+ expect(deriveResourceFromCommandId('mod.children.update')).toBe('mod.child')
19
+ expect(deriveResourceFromCommandId('mod.mice.update')).toBe('mod.mouse')
20
+ expect(deriveResourceFromCommandId('mod.men.update')).toBe('mod.man')
21
+ expect(deriveResourceFromCommandId('mod.women.update')).toBe('mod.woman')
22
+ expect(deriveResourceFromCommandId('mod.geese.update')).toBe('mod.goose')
23
+ expect(deriveResourceFromCommandId('mod.feet.update')).toBe('mod.foot')
24
+ expect(deriveResourceFromCommandId('mod.teeth.update')).toBe('mod.tooth')
25
+ expect(deriveResourceFromCommandId('mod.oxen.update')).toBe('mod.ox')
26
+ })
27
+
28
+ it('lower-cases the entity segment before matching irregular plurals', () => {
29
+ // Singularizer is case-insensitive (lower-cases input); commandId module
30
+ // segment casing is preserved by the caller as today.
31
+ expect(deriveResourceFromCommandId('customers.People.update')).toBe('customers.person')
32
+ })
33
+
34
+ it('still handles each pre-existing regular plural rule', () => {
35
+ // ies → y (companies → company)
36
+ expect(deriveResourceFromCommandId('customers.companies.update')).toBe('customers.company')
37
+ // ses → s (addresses → address)
38
+ expect(deriveResourceFromCommandId('customers.addresses.update')).toBe('customers.address')
39
+ // xes → x (boxes → box)
40
+ expect(deriveResourceFromCommandId('mod.boxes.update')).toBe('mod.box')
41
+ // zes → z (quizzes → quizze — preserved as today; checking the rule still fires)
42
+ expect(deriveResourceFromCommandId('mod.buzzes.update')).toBe('mod.buzz')
43
+ // ches → ch (matches → match)
44
+ expect(deriveResourceFromCommandId('mod.matches.update')).toBe('mod.match')
45
+ // shes → sh (dishes → dish)
46
+ expect(deriveResourceFromCommandId('mod.dishes.update')).toBe('mod.dish')
47
+ // trailing s → drop (orders → order)
48
+ expect(deriveResourceFromCommandId('sales.orders.update')).toBe('sales.order')
49
+ // dashed segments singularise each part (leave-requests → leave-request)
50
+ expect(deriveResourceFromCommandId('staff.leave-requests.update')).toBe('staff.leave-request')
51
+ })
52
+
53
+ it('leaves already-singular hand-written commandIds unchanged', () => {
54
+ expect(deriveResourceFromCommandId('mod.person.update')).toBe('mod.person')
55
+ expect(deriveResourceFromCommandId('mod.box.update')).toBe('mod.box')
56
+ })
57
+
58
+ it('returns null for malformed inputs', () => {
59
+ expect(deriveResourceFromCommandId(null)).toBeNull()
60
+ expect(deriveResourceFromCommandId(undefined)).toBeNull()
61
+ expect(deriveResourceFromCommandId('')).toBeNull()
62
+ expect(deriveResourceFromCommandId('singletonly')).toBeNull()
63
+ })
64
+ })
@@ -110,8 +110,22 @@ export function pickFirstIdentifier(...values: Array<unknown>): string | null {
110
110
  return null
111
111
  }
112
112
 
113
+ const IRREGULAR_PLURALS: Record<string, string> = {
114
+ people: 'person',
115
+ children: 'child',
116
+ mice: 'mouse',
117
+ men: 'man',
118
+ women: 'woman',
119
+ geese: 'goose',
120
+ feet: 'foot',
121
+ teeth: 'tooth',
122
+ oxen: 'ox',
123
+ }
124
+
113
125
  function singularizeSegment(segment: string): string {
114
126
  const lower = segment.toLowerCase()
127
+ const irregular = IRREGULAR_PLURALS[lower]
128
+ if (irregular) return irregular
115
129
  if (lower.endsWith('ies') && lower.length > 3) return lower.slice(0, -3) + 'y'
116
130
  if (lower.endsWith('ses') && lower.length > 3) return lower.slice(0, -2)
117
131
  if (