@open-mercato/shared 0.8.1-develop.7227.1.9248c14599 → 0.8.1-develop.7237.1.635faf2c7a
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/availability/catalogOnlyProvider.js +65 -0
- package/dist/lib/availability/catalogOnlyProvider.js.map +7 -0
- package/dist/lib/availability/index.js +8 -0
- package/dist/lib/availability/index.js.map +7 -0
- package/dist/lib/availability/registry.js +47 -0
- package/dist/lib/availability/registry.js.map +7 -0
- package/dist/lib/availability/types.js +7 -0
- package/dist/lib/availability/types.js.map +7 -0
- package/dist/lib/crud/factory.js +21 -7
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/db/pg-errors.js +9 -1
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/encryption/aes.js +14 -0
- package/dist/lib/encryption/aes.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +17 -1
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +6 -2
- package/src/lib/availability/__tests__/catalogOnlyProvider.test.ts +98 -0
- package/src/lib/availability/__tests__/registry.test.ts +115 -0
- package/src/lib/availability/catalogOnlyProvider.ts +119 -0
- package/src/lib/availability/index.ts +18 -0
- package/src/lib/availability/registry.ts +120 -0
- package/src/lib/availability/types.ts +71 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +91 -4
- package/src/lib/crud/factory.ts +37 -6
- package/src/lib/db/__tests__/pg-errors.test.ts +31 -1
- package/src/lib/db/pg-errors.ts +23 -0
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +97 -9
- package/src/lib/encryption/aes.ts +43 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +44 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 297 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { availabilityItemKey } from "./types.js";
|
|
2
|
+
import { availabilityProviderRegistry, AVAILABILITY_CATALOG_ONLY_PROVIDER_ID } from "./registry.js";
|
|
3
|
+
let policyLookup = null;
|
|
4
|
+
function setCatalogOnlyPolicyLookup(lookup) {
|
|
5
|
+
policyLookup = lookup;
|
|
6
|
+
}
|
|
7
|
+
function pureFallbackItem() {
|
|
8
|
+
return {
|
|
9
|
+
state: "not_tracked",
|
|
10
|
+
availableQuantity: null,
|
|
11
|
+
canFulfil: true,
|
|
12
|
+
leadTimeDays: null,
|
|
13
|
+
releaseAt: null,
|
|
14
|
+
isAuthoritative: true,
|
|
15
|
+
policySourceId: null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function applyOverride(override) {
|
|
19
|
+
const base = pureFallbackItem();
|
|
20
|
+
if (!override) return base;
|
|
21
|
+
const policySourceId = override.policySourceId ?? null;
|
|
22
|
+
if (override.preorderReleaseAt) {
|
|
23
|
+
const releaseAt = new Date(override.preorderReleaseAt);
|
|
24
|
+
if (!Number.isNaN(releaseAt.getTime()) && releaseAt.getTime() > Date.now()) {
|
|
25
|
+
return {
|
|
26
|
+
...base,
|
|
27
|
+
state: "preorder",
|
|
28
|
+
canFulfil: true,
|
|
29
|
+
releaseAt: override.preorderReleaseAt,
|
|
30
|
+
policySourceId
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (override.isActive === false) {
|
|
35
|
+
return { ...base, state: "out_of_stock", canFulfil: false, policySourceId };
|
|
36
|
+
}
|
|
37
|
+
if (override.isStockManaged === true) {
|
|
38
|
+
return { ...base, state: "out_of_stock", canFulfil: false, policySourceId };
|
|
39
|
+
}
|
|
40
|
+
return { ...base, policySourceId };
|
|
41
|
+
}
|
|
42
|
+
async function getAvailability(query) {
|
|
43
|
+
const byItem = {};
|
|
44
|
+
let overrides = {};
|
|
45
|
+
if (policyLookup) {
|
|
46
|
+
try {
|
|
47
|
+
overrides = await policyLookup(query);
|
|
48
|
+
} catch {
|
|
49
|
+
overrides = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const item of query.items) {
|
|
53
|
+
const key = availabilityItemKey(item);
|
|
54
|
+
byItem[key] = applyOverride(overrides[key]);
|
|
55
|
+
}
|
|
56
|
+
return { byItem };
|
|
57
|
+
}
|
|
58
|
+
availabilityProviderRegistry.register({
|
|
59
|
+
id: AVAILABILITY_CATALOG_ONLY_PROVIDER_ID,
|
|
60
|
+
getAvailability
|
|
61
|
+
});
|
|
62
|
+
export {
|
|
63
|
+
setCatalogOnlyPolicyLookup
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=catalogOnlyProvider.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/availability/catalogOnlyProvider.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The built-in `catalog-only` fallback provider. Always registered so a\n * `wms`-less *and* `availability`-less storefront stays fully functional.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A74.3\n */\n\nimport { availabilityItemKey } from './types'\nimport type { AvailabilityItemResult, AvailabilityQuery, AvailabilityResult } from './types'\nimport { availabilityProviderRegistry, AVAILABILITY_CATALOG_ONLY_PROVIDER_ID } from './registry'\n\n/** Per-item policy signal the optional lookup hook may report. */\nexport type CatalogOnlyPolicyOverride = {\n /** `true` \u2192 the item is explicitly opted into stock tracking with no data source behind it (\u00A74.3, out_of_stock). */\n isStockManaged?: boolean\n /** `false` \u2192 the policy row is inactive; treated as out_of_stock. */\n isActive?: boolean\n /** ISO-8601. In the future \u2192 `preorder`. */\n preorderReleaseAt?: string | null\n /** The `AvailabilityPolicy` row id that produced this override. */\n policySourceId?: string | null\n}\n\n/**\n * Optional soft lookup into `AvailabilityPolicy` when the `availability`\n * module is installed. Keyed by `availabilityItemKey()`. Returning `null` or\n * omitting an item from the map means \"no policy row \u2014 module default\".\n */\nexport type CatalogOnlyPolicyLookup = (\n query: AvailabilityQuery,\n) => Promise<Record<string, CatalogOnlyPolicyOverride | null | undefined>>\n\nlet policyLookup: CatalogOnlyPolicyLookup | null = null\n\n/**\n * Wired by the `availability` module's `di.ts` at container-build time\n * (closure captures the container, mirroring `wms/di.ts`'s own provider\n * registration) \u2014 never a static import from `packages/shared`. Pass `null`\n * to clear (test isolation).\n */\nexport function setCatalogOnlyPolicyLookup(lookup: CatalogOnlyPolicyLookup | null): void {\n policyLookup = lookup\n}\n\nfunction pureFallbackItem(): AvailabilityItemResult {\n return {\n state: 'not_tracked',\n availableQuantity: null,\n canFulfil: true,\n leadTimeDays: null,\n releaseAt: null,\n isAuthoritative: true,\n policySourceId: null,\n }\n}\n\n/**\n * Applies decision 7's matrix (see PLAN.md \u00A7 Key design decisions) for a\n * resolved policy override on top of the pure fallback.\n */\nfunction applyOverride(override: CatalogOnlyPolicyOverride | null | undefined): AvailabilityItemResult {\n const base = pureFallbackItem()\n if (!override) return base\n\n const policySourceId = override.policySourceId ?? null\n\n if (override.preorderReleaseAt) {\n const releaseAt = new Date(override.preorderReleaseAt)\n if (!Number.isNaN(releaseAt.getTime()) && releaseAt.getTime() > Date.now()) {\n return {\n ...base,\n state: 'preorder',\n canFulfil: true,\n releaseAt: override.preorderReleaseAt,\n policySourceId,\n }\n }\n }\n\n if (override.isActive === false) {\n return { ...base, state: 'out_of_stock', canFulfil: false, policySourceId }\n }\n\n if (override.isStockManaged === true) {\n // Opted into stock tracking with no data source to verify against \u2014 see\n // decision 7: this is the \"policy explicitly marks the item unavailable\"\n // case rather than a silent \"in stock\" claim (R5).\n return { ...base, state: 'out_of_stock', canFulfil: false, policySourceId }\n }\n\n return { ...base, policySourceId }\n}\n\nasync function getAvailability(query: AvailabilityQuery): Promise<AvailabilityResult> {\n const byItem: AvailabilityResult['byItem'] = {}\n\n let overrides: Record<string, CatalogOnlyPolicyOverride | null | undefined> = {}\n if (policyLookup) {\n try {\n overrides = await policyLookup(query)\n } catch {\n // Degrade gracefully to the pure fallback \u2014 never let an optional\n // policy lookup failure break the always-available fallback provider.\n overrides = {}\n }\n }\n\n for (const item of query.items) {\n const key = availabilityItemKey(item)\n byItem[key] = applyOverride(overrides[key])\n }\n\n return { byItem }\n}\n\navailabilityProviderRegistry.register({\n id: AVAILABILITY_CATALOG_ONLY_PROVIDER_ID,\n getAvailability,\n})\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,2BAA2B;AAEpC,SAAS,8BAA8B,6CAA6C;AAuBpF,IAAI,eAA+C;AAQ5C,SAAS,2BAA2B,QAA8C;AACvF,iBAAe;AACjB;AAEA,SAAS,mBAA2C;AAClD,SAAO;AAAA,IACL,OAAO;AAAA,IACP,mBAAmB;AAAA,IACnB,WAAW;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,EAClB;AACF;AAMA,SAAS,cAAc,UAAgF;AACrG,QAAM,OAAO,iBAAiB;AAC9B,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,iBAAiB,SAAS,kBAAkB;AAElD,MAAI,SAAS,mBAAmB;AAC9B,UAAM,YAAY,IAAI,KAAK,SAAS,iBAAiB;AACrD,QAAI,CAAC,OAAO,MAAM,UAAU,QAAQ,CAAC,KAAK,UAAU,QAAQ,IAAI,KAAK,IAAI,GAAG;AAC1E,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,QACP,WAAW;AAAA,QACX,WAAW,SAAS;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,aAAa,OAAO;AAC/B,WAAO,EAAE,GAAG,MAAM,OAAO,gBAAgB,WAAW,OAAO,eAAe;AAAA,EAC5E;AAEA,MAAI,SAAS,mBAAmB,MAAM;AAIpC,WAAO,EAAE,GAAG,MAAM,OAAO,gBAAgB,WAAW,OAAO,eAAe;AAAA,EAC5E;AAEA,SAAO,EAAE,GAAG,MAAM,eAAe;AACnC;AAEA,eAAe,gBAAgB,OAAuD;AACpF,QAAM,SAAuC,CAAC;AAE9C,MAAI,YAA0E,CAAC;AAC/E,MAAI,cAAc;AAChB,QAAI;AACF,kBAAY,MAAM,aAAa,KAAK;AAAA,IACtC,QAAQ;AAGN,kBAAY,CAAC;AAAA,IACf;AAAA,EACF;AAEA,aAAW,QAAQ,MAAM,OAAO;AAC9B,UAAM,MAAM,oBAAoB,IAAI;AACpC,WAAO,GAAG,IAAI,cAAc,UAAU,GAAG,CAAC;AAAA,EAC5C;AAEA,SAAO,EAAE,OAAO;AAClB;AAEA,6BAA6B,SAAS;AAAA,EACpC,IAAI;AAAA,EACJ;AACF,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/availability/index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Public entrypoint for the Availability Contract's shared, dependency-free\n * base \u2014 types, the provider registry, `resolveAvailability()`, and the\n * built-in `catalog-only` fallback.\n *\n * Importing from this barrel (rather than `./registry` directly) guarantees\n * the `catalog-only` provider is registered \u2014 it self-registers as an import\n * side effect in `./catalogOnlyProvider`, which this file always pulls in.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A74.1a\n */\n\nexport * from './types'\nexport * from './registry'\nexport { setCatalogOnlyPolicyLookup } from './catalogOnlyProvider'\nexport type { CatalogOnlyPolicyLookup, CatalogOnlyPolicyOverride } from './catalogOnlyProvider'\n\nimport './catalogOnlyProvider'\n"],
|
|
5
|
+
"mappings": "AAYA,cAAc;AACd,cAAc;AACd,SAAS,kCAAkC;AAG3C,OAAO;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const AVAILABILITY_CATALOG_ONLY_PROVIDER_ID = "catalog-only";
|
|
2
|
+
class AvailabilityProviderRegistryImpl {
|
|
3
|
+
constructor() {
|
|
4
|
+
// Preserves registration order via Map iteration semantics.
|
|
5
|
+
this.providers = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
register(provider) {
|
|
8
|
+
if (!provider || typeof provider.id !== "string" || provider.id.length === 0) {
|
|
9
|
+
throw new Error("[internal] AvailabilityProviderRegistry: provider must have a non-empty id");
|
|
10
|
+
}
|
|
11
|
+
this.providers.set(provider.id, provider);
|
|
12
|
+
}
|
|
13
|
+
get(id) {
|
|
14
|
+
return this.providers.get(id) ?? null;
|
|
15
|
+
}
|
|
16
|
+
list() {
|
|
17
|
+
return Array.from(this.providers.values());
|
|
18
|
+
}
|
|
19
|
+
reset() {
|
|
20
|
+
this.providers.clear();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const availabilityProviderRegistry = new AvailabilityProviderRegistryImpl();
|
|
24
|
+
function resolveAutoProvider() {
|
|
25
|
+
for (const provider of availabilityProviderRegistry.list()) {
|
|
26
|
+
if (provider.id !== AVAILABILITY_CATALOG_ONLY_PROVIDER_ID) return provider;
|
|
27
|
+
}
|
|
28
|
+
return availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID);
|
|
29
|
+
}
|
|
30
|
+
async function resolveAvailability(query, options) {
|
|
31
|
+
const selection = options?.moduleConfig ? await options.moduleConfig.getValue("availability", "selectedProvider", {
|
|
32
|
+
defaultValue: "auto",
|
|
33
|
+
scope: { tenantId: query.tenantId }
|
|
34
|
+
}) : "auto";
|
|
35
|
+
const provider = !selection || selection === "auto" ? resolveAutoProvider() : availabilityProviderRegistry.get(selection) ?? availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID);
|
|
36
|
+
const resolved = provider ?? availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID);
|
|
37
|
+
if (!resolved) {
|
|
38
|
+
throw new Error("[internal] No availability provider registered, including the built-in catalog-only fallback");
|
|
39
|
+
}
|
|
40
|
+
return resolved.getAvailability(query);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
AVAILABILITY_CATALOG_ONLY_PROVIDER_ID,
|
|
44
|
+
availabilityProviderRegistry,
|
|
45
|
+
resolveAvailability
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/availability/registry.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Availability provider registry \u2014 module-level singleton that collects\n * registered providers and dispatches `resolveAvailability()` to the\n * per-tenant selected one, falling back safely to the built-in\n * `catalog-only` provider.\n *\n * Mirrors `packages/shared/src/lib/ai/llm-provider-registry.ts`.\n *\n * @see ./types\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A74.1a\n */\n\nimport type { AvailabilityProvider, AvailabilityQuery, AvailabilityResult } from './types'\n\nexport const AVAILABILITY_CATALOG_ONLY_PROVIDER_ID = 'catalog-only'\n\n/** Public interface of the registry. Exposed as a singleton via {@link availabilityProviderRegistry}. */\nexport interface AvailabilityProviderRegistry {\n /**\n * Registers or replaces a provider. Registration is idempotent \u2014 calling\n * with the same id replaces the existing entry. Never order-dependent.\n */\n register(provider: AvailabilityProvider): void\n\n /** Returns the provider with the given id, or null when not registered. */\n get(id: string): AvailabilityProvider | null\n\n /** Returns all registered providers in registration order. */\n list(): readonly AvailabilityProvider[]\n\n /** Removes all registered providers. Intended for test isolation. */\n reset(): void\n}\n\nclass AvailabilityProviderRegistryImpl implements AvailabilityProviderRegistry {\n // Preserves registration order via Map iteration semantics.\n private readonly providers = new Map<string, AvailabilityProvider>()\n\n register(provider: AvailabilityProvider): void {\n if (!provider || typeof provider.id !== 'string' || provider.id.length === 0) {\n throw new Error('[internal] AvailabilityProviderRegistry: provider must have a non-empty id')\n }\n // Idempotent: replace existing by id.\n this.providers.set(provider.id, provider)\n }\n\n get(id: string): AvailabilityProvider | null {\n return this.providers.get(id) ?? null\n }\n\n list(): readonly AvailabilityProvider[] {\n return Array.from(this.providers.values())\n }\n\n reset(): void {\n this.providers.clear()\n }\n}\n\n/** Process-level singleton instance of the registry. */\nexport const availabilityProviderRegistry: AvailabilityProviderRegistry = new AvailabilityProviderRegistryImpl()\n\nexport type AvailabilityProviderSelection = 'auto' | 'catalog-only' | (string & {})\n\n/**\n * Narrow port for per-tenant provider selection \u2014 declared locally so this\n * package stays dependency-free. A caller with DI access (an `availability`\n * module route, a future `ecommerce`/`cart`/`checkout` consumer) passes in\n * its resolved `ModuleConfigService` instance; omitting it always resolves\n * `'auto'`.\n */\nexport interface AvailabilityModuleConfigReader {\n getValue<T = unknown>(\n moduleId: string,\n name: string,\n options?: { defaultValue?: T | null; scope?: { tenantId?: string | null; organizationId?: string | null } },\n ): Promise<T | null>\n}\n\nexport interface ResolveAvailabilityOptions {\n /** `ModuleConfigService('availability', 'selectedProvider')` reader. See {@link AvailabilityModuleConfigReader}. */\n moduleConfig?: AvailabilityModuleConfigReader\n}\n\nfunction resolveAutoProvider(): AvailabilityProvider | null {\n // 'auto' = \"the highest-precedence registered provider\" \u2014 the first\n // non-catalog-only registrant, in registration order.\n for (const provider of availabilityProviderRegistry.list()) {\n if (provider.id !== AVAILABILITY_CATALOG_ONLY_PROVIDER_ID) return provider\n }\n return availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID)\n}\n\n/**\n * The entry point every read-side consumer calls. Advisory only \u2014 see \u00A74.1:\n * `resolveAvailability()` is never a stock guarantee, `reserveAvailability()`\n * (the `availability` module, not shipped by this contract) is.\n */\nexport async function resolveAvailability(\n query: AvailabilityQuery,\n options?: ResolveAvailabilityOptions,\n): Promise<AvailabilityResult> {\n const selection = options?.moduleConfig\n ? await options.moduleConfig.getValue<AvailabilityProviderSelection>('availability', 'selectedProvider', {\n defaultValue: 'auto',\n scope: { tenantId: query.tenantId },\n })\n : 'auto'\n\n const provider =\n !selection || selection === 'auto'\n ? resolveAutoProvider()\n : availabilityProviderRegistry.get(selection) ?? availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID)\n\n const resolved = provider ?? availabilityProviderRegistry.get(AVAILABILITY_CATALOG_ONLY_PROVIDER_ID)\n if (!resolved) {\n throw new Error('[internal] No availability provider registered, including the built-in catalog-only fallback')\n }\n return resolved.getAvailability(query)\n}\n"],
|
|
5
|
+
"mappings": "AAcO,MAAM,wCAAwC;AAoBrD,MAAM,iCAAyE;AAAA,EAA/E;AAEE;AAAA,SAAiB,YAAY,oBAAI,IAAkC;AAAA;AAAA,EAEnE,SAAS,UAAsC;AAC7C,QAAI,CAAC,YAAY,OAAO,SAAS,OAAO,YAAY,SAAS,GAAG,WAAW,GAAG;AAC5E,YAAM,IAAI,MAAM,4EAA4E;AAAA,IAC9F;AAEA,SAAK,UAAU,IAAI,SAAS,IAAI,QAAQ;AAAA,EAC1C;AAAA,EAEA,IAAI,IAAyC;AAC3C,WAAO,KAAK,UAAU,IAAI,EAAE,KAAK;AAAA,EACnC;AAAA,EAEA,OAAwC;AACtC,WAAO,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,EAC3C;AAAA,EAEA,QAAc;AACZ,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;AAGO,MAAM,+BAA6D,IAAI,iCAAiC;AAwB/G,SAAS,sBAAmD;AAG1D,aAAW,YAAY,6BAA6B,KAAK,GAAG;AAC1D,QAAI,SAAS,OAAO,sCAAuC,QAAO;AAAA,EACpE;AACA,SAAO,6BAA6B,IAAI,qCAAqC;AAC/E;AAOA,eAAsB,oBACpB,OACA,SAC6B;AAC7B,QAAM,YAAY,SAAS,eACvB,MAAM,QAAQ,aAAa,SAAwC,gBAAgB,oBAAoB;AAAA,IACrG,cAAc;AAAA,IACd,OAAO,EAAE,UAAU,MAAM,SAAS;AAAA,EACpC,CAAC,IACD;AAEJ,QAAM,WACJ,CAAC,aAAa,cAAc,SACxB,oBAAoB,IACpB,6BAA6B,IAAI,SAAS,KAAK,6BAA6B,IAAI,qCAAqC;AAE3H,QAAM,WAAW,YAAY,6BAA6B,IAAI,qCAAqC;AACnG,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,8FAA8F;AAAA,EAChH;AACA,SAAO,SAAS,gBAAgB,KAAK;AACvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/availability/types.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Availability Contract \u2014 base types.\n *\n * Zero module dependencies, so `ecommerce`, `cart`, `checkout` and `catalog`\n * can consume this without a `requires` edge on either `availability` or `wms`.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A74.1a\n */\n\nexport type AvailabilityState =\n | 'in_stock'\n | 'low_stock'\n | 'out_of_stock'\n | 'backorder'\n | 'preorder'\n | 'not_tracked'\n\nexport type AvailabilityItemQuery = {\n catalogProductId: string\n /** `null` or omitted \u2192 product-level rollup over active variants. */\n catalogVariantId?: string | null\n /** The quantity being asked about; state is relative to it. */\n quantity: number\n}\n\nexport type AvailabilityQuery = {\n tenantId: string\n organizationId: string\n items: AvailabilityItemQuery[]\n /** Selects the policy chain. */\n storeId?: string | null\n channelId?: string | null\n /** `null`/omitted \u2192 every in-scope location. */\n locationIds?: string[] | null\n /**\n * Additive, optional. When true, a provider MUST skip any internal read\n * cache and compute a live result \u2014 the \u00A76 \"cart re-validation\" row.\n */\n bypassCache?: boolean\n}\n\nexport type AvailabilityItemResult = {\n state: AvailabilityState\n /** Sellable quantity; `null` when not tracked. */\n availableQuantity: number | null\n /** Whether the requested quantity can be met, including a backorder/preorder path. */\n canFulfil: boolean\n /** Set for `'backorder'`. */\n leadTimeDays: number | null\n /** ISO-8601; set for `'preorder'`. */\n releaseAt: string | null\n /** `false` for a cached browse-time read \u2014 never a guarantee. */\n isAuthoritative: boolean\n /** The `AvailabilityPolicy` row that decided, or `null` for a module default. */\n policySourceId: string | null\n}\n\nexport type AvailabilityResult = {\n /** Key: `${catalogProductId}:${catalogVariantId ?? ''}` \u2014 stable and caller-derivable. */\n byItem: Record<string, AvailabilityItemResult>\n}\n\nexport interface AvailabilityProvider {\n id: string\n getAvailability(query: AvailabilityQuery): Promise<AvailabilityResult>\n}\n\n/** Builds the stable `AvailabilityResult.byItem` key for an item. */\nexport function availabilityItemKey(item: { catalogProductId: string; catalogVariantId?: string | null }): string {\n return `${item.catalogProductId}:${item.catalogVariantId ?? ''}`\n}\n"],
|
|
5
|
+
"mappings": "AAoEO,SAAS,oBAAoB,MAA8E;AAChH,SAAO,GAAG,KAAK,gBAAgB,IAAI,KAAK,oBAAoB,EAAE;AAChE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/crud/factory.js
CHANGED
|
@@ -54,9 +54,10 @@ import { parseExtensionHeaders } from "../umes/extension-headers.js";
|
|
|
54
54
|
import { createGenericOptimisticLockReader } from "./optimistic-lock.js";
|
|
55
55
|
import { registerOptimisticLockReaderIfAbsent } from "./optimistic-lock-store.js";
|
|
56
56
|
import { createLogger } from "../logger/index.js";
|
|
57
|
-
import { getForeignKeyViolationConstraint, isForeignKeyViolation, isTransientDbError } from "../db/pg-errors.js";
|
|
57
|
+
import { getForeignKeyViolationConstraint, isForeignKeyViolation, isTransientDbError, readPgSqlState } from "../db/pg-errors.js";
|
|
58
58
|
import { getTelemetryRuntime } from "../telemetry/runtime.js";
|
|
59
59
|
import { randomUUID } from "node:crypto";
|
|
60
|
+
import { NotFoundError as MikroOrmNotFoundError, ValidationError as MikroOrmValidationError } from "@mikro-orm/core";
|
|
60
61
|
const logger = createLogger("shared").child({ component: "crud" });
|
|
61
62
|
function resolveSortParams(queryParams, defaultSort) {
|
|
62
63
|
const rawSortField = queryParams.sortField ?? queryParams.sort;
|
|
@@ -307,6 +308,14 @@ function resolveRequestId(request) {
|
|
|
307
308
|
if (inbound && INBOUND_REQUEST_ID_PATTERN.test(inbound)) return inbound;
|
|
308
309
|
return randomUUID();
|
|
309
310
|
}
|
|
311
|
+
function classifyCrudError(err) {
|
|
312
|
+
const pgSqlState = readPgSqlState(err);
|
|
313
|
+
if (pgSqlState) return { code: "DATABASE_ERROR", pgSqlState };
|
|
314
|
+
if (err instanceof MikroOrmValidationError || err instanceof MikroOrmNotFoundError) {
|
|
315
|
+
return { code: "PERSISTENCE_ERROR", pgSqlState: null };
|
|
316
|
+
}
|
|
317
|
+
return { code: "INTERNAL_ERROR", pgSqlState: null };
|
|
318
|
+
}
|
|
310
319
|
function handleError(err, request) {
|
|
311
320
|
if (err instanceof Response) return err;
|
|
312
321
|
if (isCrudHttpError(err)) return json(err.body, { status: err.status });
|
|
@@ -316,12 +325,14 @@ function handleError(err, request) {
|
|
|
316
325
|
}
|
|
317
326
|
if (err instanceof z.ZodError) return json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
318
327
|
if (isTransientDbError(err)) {
|
|
328
|
+
const requestId2 = resolveRequestId(request);
|
|
319
329
|
logger.warn("Transient DB failure during CRUD handler", {
|
|
320
|
-
message: err instanceof Error ? err.message : void 0
|
|
330
|
+
message: err instanceof Error ? err.message : void 0,
|
|
331
|
+
requestId: requestId2
|
|
321
332
|
});
|
|
322
333
|
return json(
|
|
323
|
-
{ error: "Service temporarily unavailable" },
|
|
324
|
-
{ status: 503, headers: { "Retry-After": "2" } }
|
|
334
|
+
{ error: "Service temporarily unavailable", code: "DATABASE_UNAVAILABLE", requestId: requestId2 },
|
|
335
|
+
{ status: 503, headers: { "Retry-After": "2", "x-request-id": requestId2 } }
|
|
325
336
|
);
|
|
326
337
|
}
|
|
327
338
|
if (isForeignKeyViolation(err)) {
|
|
@@ -349,15 +360,18 @@ function handleError(err, request) {
|
|
|
349
360
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
350
361
|
const errorName = err instanceof Error ? err.name : void 0;
|
|
351
362
|
const requestId = resolveRequestId(request);
|
|
352
|
-
|
|
363
|
+
const { code, pgSqlState } = classifyCrudError(err);
|
|
364
|
+
logger.error("Unexpected CRUD error", { message, stack, err, requestId, code });
|
|
353
365
|
getTelemetryRuntime()?.reportError(err, {
|
|
354
366
|
module: "crud",
|
|
355
|
-
|
|
367
|
+
code: `crud.${code.toLowerCase()}`,
|
|
368
|
+
attributes: { requestId, errorName, code, pgCode: pgSqlState ?? void 0 }
|
|
356
369
|
});
|
|
357
370
|
const body = {
|
|
358
371
|
error: "Internal server error",
|
|
359
372
|
message: "Something went wrong. Please try again later.",
|
|
360
|
-
requestId
|
|
373
|
+
requestId,
|
|
374
|
+
code
|
|
361
375
|
};
|
|
362
376
|
return json(body, { status: 500, headers: { "x-request-id": requestId } });
|
|
363
377
|
}
|