@porulle/core 0.48.1 → 0.50.0
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/modules/audit/hooks.d.ts +0 -4
- package/dist/modules/audit/hooks.d.ts.map +1 -1
- package/dist/modules/audit/hooks.js +17 -0
- package/dist/modules/catalog/entity-service.d.ts +2 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +1 -1
- package/dist/modules/catalog/import-service.d.ts +108 -0
- package/dist/modules/catalog/import-service.d.ts.map +1 -0
- package/dist/modules/catalog/import-service.js +423 -0
- package/dist/modules/catalog/service.d.ts +8 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/channels/catalog-schema.d.ts +130 -0
- package/dist/modules/channels/catalog-schema.d.ts.map +1 -0
- package/dist/modules/channels/catalog-schema.js +68 -0
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +13 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/index.ts +21 -0
- package/src/modules/audit/hooks.ts +18 -0
- package/src/modules/catalog/entity-service.ts +1 -1
- package/src/modules/catalog/import-service.ts +563 -0
- package/src/modules/catalog/service.ts +22 -0
- package/src/modules/channels/catalog-schema.ts +97 -0
- package/src/modules/inventory/service.ts +13 -0
- package/src/testing.ts +3 -0
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
import { EntityService } from "./entity-service.js";
|
|
60
60
|
import { CategoryService } from "./category-service.js";
|
|
61
61
|
import { BrandService } from "./brand-service.js";
|
|
62
|
+
import { CatalogImportService, type ImportProduct, type ImportProductsOptions, type ImportProductsReport } from "./import-service.js";
|
|
62
63
|
|
|
63
64
|
// ─── Hand-written types (not derivable from a single z.infer) ───────────────
|
|
64
65
|
|
|
@@ -232,6 +233,16 @@ export interface CatalogService {
|
|
|
232
233
|
actor: Actor | null,
|
|
233
234
|
ctx?: TxContext,
|
|
234
235
|
): Promise<Result<CatalogEntityHydrated>>;
|
|
236
|
+
/**
|
|
237
|
+
* The import fast path: a page of NEW products in one transaction, per-item savepoints, one
|
|
238
|
+
* revision per item, one `catalog.afterImport` hook per page. See `import-service.ts`.
|
|
239
|
+
*/
|
|
240
|
+
importProducts(
|
|
241
|
+
page: ImportProduct[],
|
|
242
|
+
options: ImportProductsOptions,
|
|
243
|
+
actor: Actor | null,
|
|
244
|
+
ctx?: CatalogWriteContext,
|
|
245
|
+
): Promise<Result<ImportProductsReport>>;
|
|
235
246
|
discontinue(
|
|
236
247
|
id: string,
|
|
237
248
|
actor: Actor | null,
|
|
@@ -445,6 +456,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
445
456
|
private readonly entities: EntityService;
|
|
446
457
|
private readonly categories: CategoryService;
|
|
447
458
|
private readonly brands: BrandService;
|
|
459
|
+
private readonly importer: CatalogImportService;
|
|
448
460
|
|
|
449
461
|
constructor(deps: CatalogServiceDeps) {
|
|
450
462
|
this.repository = deps.repository;
|
|
@@ -453,6 +465,7 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
453
465
|
this.entities = new EntityService(deps, this.resolveEntityFieldDefinitions.bind(this));
|
|
454
466
|
this.categories = new CategoryService(deps);
|
|
455
467
|
this.brands = new BrandService(deps);
|
|
468
|
+
this.importer = new CatalogImportService(deps);
|
|
456
469
|
}
|
|
457
470
|
|
|
458
471
|
private codeFieldDefinition(entityType: string, name: string): EntityFieldDefinition | undefined {
|
|
@@ -990,6 +1003,15 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
990
1003
|
});
|
|
991
1004
|
}
|
|
992
1005
|
|
|
1006
|
+
importProducts(
|
|
1007
|
+
page: ImportProduct[],
|
|
1008
|
+
options: ImportProductsOptions,
|
|
1009
|
+
actor: Actor | null,
|
|
1010
|
+
ctx?: CatalogWriteContext,
|
|
1011
|
+
): Promise<Result<ImportProductsReport>> {
|
|
1012
|
+
return this.importer.importProducts(page, options, actor, ctx);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
993
1015
|
archive(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
|
|
994
1016
|
return this.withMutationResult(actor, ctx, async (txCtx) => {
|
|
995
1017
|
const result = await this.entities.archive(id, actor, txCtx);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelCatalogImage,
|
|
4
|
+
ChannelCatalogItem,
|
|
5
|
+
ChannelCatalogLocalizedAttributes,
|
|
6
|
+
ChannelCatalogOptionType,
|
|
7
|
+
ChannelCatalogPrice,
|
|
8
|
+
ChannelCatalogVariant,
|
|
9
|
+
ChannelInventoryLevel,
|
|
10
|
+
} from "./adapter.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The parser for a catalog item that crossed a trust boundary — a queue message, an R2 object, a
|
|
14
|
+
* JSON fixture. Import this rather than re-declaring a subset: a consumer's hand-written schema that
|
|
15
|
+
* kept only the fields it knew about once converged 96 products with no options, prices or
|
|
16
|
+
* inventory, and a reconcile then "re-converged" all of them. The `Equals` checks at the bottom make
|
|
17
|
+
* this schema fail to compile the day it and the interfaces disagree.
|
|
18
|
+
*/
|
|
19
|
+
const record = z.record(z.string(), z.unknown());
|
|
20
|
+
|
|
21
|
+
export const channelCatalogLocalizedAttributesSchema = z.object({
|
|
22
|
+
locale: z.string(),
|
|
23
|
+
title: z.string(),
|
|
24
|
+
subtitle: z.string().exactOptional(),
|
|
25
|
+
description: z.string().exactOptional(),
|
|
26
|
+
richDescription: z.unknown().exactOptional(),
|
|
27
|
+
seoTitle: z.string().exactOptional(),
|
|
28
|
+
seoDescription: z.string().exactOptional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const channelCatalogImageSchema = z.object({
|
|
32
|
+
externalId: z.string().exactOptional(),
|
|
33
|
+
url: z.string(),
|
|
34
|
+
alt: z.string().exactOptional(),
|
|
35
|
+
role: z.enum(["primary", "gallery", "thumbnail", "video", "document"]),
|
|
36
|
+
sortOrder: z.number().exactOptional(),
|
|
37
|
+
variantExternalIds: z.array(z.string()).exactOptional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const channelCatalogOptionTypeSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
displayName: z.string(),
|
|
43
|
+
sortOrder: z.number().exactOptional(),
|
|
44
|
+
values: z.array(z.object({
|
|
45
|
+
value: z.string(),
|
|
46
|
+
displayValue: z.string(),
|
|
47
|
+
sortOrder: z.number().exactOptional(),
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const channelCatalogPriceSchema = z.object({
|
|
52
|
+
currency: z.string(),
|
|
53
|
+
amount: z.number(),
|
|
54
|
+
compareAtAmount: z.number().exactOptional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const channelCatalogVariantSchema = z.object({
|
|
58
|
+
externalId: z.string(),
|
|
59
|
+
sku: z.string().exactOptional(),
|
|
60
|
+
barcode: z.string().exactOptional(),
|
|
61
|
+
metadata: record.exactOptional(),
|
|
62
|
+
optionValues: z.record(z.string(), z.string()).exactOptional(),
|
|
63
|
+
prices: z.array(channelCatalogPriceSchema).exactOptional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const channelCatalogItemSchema = z.object({
|
|
67
|
+
externalId: z.string(),
|
|
68
|
+
slug: z.string(),
|
|
69
|
+
title: z.string(),
|
|
70
|
+
description: z.string().exactOptional(),
|
|
71
|
+
variants: z.array(channelCatalogVariantSchema),
|
|
72
|
+
metadata: record.exactOptional(),
|
|
73
|
+
attributes: z.array(channelCatalogLocalizedAttributesSchema).exactOptional(),
|
|
74
|
+
images: z.array(channelCatalogImageSchema).exactOptional(),
|
|
75
|
+
options: z.array(channelCatalogOptionTypeSchema).exactOptional(),
|
|
76
|
+
tags: z.array(z.string()).exactOptional(),
|
|
77
|
+
brand: z.string().exactOptional(),
|
|
78
|
+
categories: z.array(z.string()).exactOptional(),
|
|
79
|
+
status: z.enum(["draft", "active", "archived", "discontinued"]).exactOptional(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const channelInventoryLevelSchema = z.object({
|
|
83
|
+
externalId: z.string(),
|
|
84
|
+
available: z.number(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
|
|
88
|
+
type Assert<T extends true> = T;
|
|
89
|
+
export type ChannelCatalogSchemaChecks = [
|
|
90
|
+
Assert<Equals<z.infer<typeof channelCatalogLocalizedAttributesSchema>, ChannelCatalogLocalizedAttributes>>,
|
|
91
|
+
Assert<Equals<z.infer<typeof channelCatalogImageSchema>, ChannelCatalogImage>>,
|
|
92
|
+
Assert<Equals<z.infer<typeof channelCatalogOptionTypeSchema>, ChannelCatalogOptionType>>,
|
|
93
|
+
Assert<Equals<z.infer<typeof channelCatalogPriceSchema>, ChannelCatalogPrice>>,
|
|
94
|
+
Assert<Equals<z.infer<typeof channelCatalogVariantSchema>, ChannelCatalogVariant>>,
|
|
95
|
+
Assert<Equals<z.infer<typeof channelCatalogItemSchema>, ChannelCatalogItem>>,
|
|
96
|
+
Assert<Equals<z.infer<typeof channelInventoryLevelSchema>, ChannelInventoryLevel>>,
|
|
97
|
+
];
|
|
@@ -503,6 +503,19 @@ export class InventoryService {
|
|
|
503
503
|
ctx,
|
|
504
504
|
);
|
|
505
505
|
|
|
506
|
+
// Already at the quantity: nothing to record. A zero-delta adjust would still write a movement
|
|
507
|
+
// and fire `inventory.afterAdjust`, and a periodic reconcile then re-announces every level it
|
|
508
|
+
// walked — 1,243 of them on one sim reconcile, each one a downstream re-projection.
|
|
509
|
+
// The permission still gates it, so a caller without `inventory:adjust` is refused either way.
|
|
510
|
+
if (existingLevel && existingLevel.quantityOnHand === input.quantity) {
|
|
511
|
+
try {
|
|
512
|
+
assertPermission(actor ?? null, "inventory:adjust");
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return Err(toCommerceError(error));
|
|
515
|
+
}
|
|
516
|
+
return Ok(existingLevel);
|
|
517
|
+
}
|
|
518
|
+
|
|
506
519
|
const currentOnHand = existingLevel?.quantityOnHand ?? 0;
|
|
507
520
|
const delta = input.quantity - currentOnHand;
|
|
508
521
|
|
package/src/testing.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { createTestKernel } from "./test-utils/create-test-kernel.js";
|
|
10
|
+
// The query log is the only honest statement counter: the PGlite logger sees every statement core
|
|
11
|
+
// issues, which a proxy around a plugin's own handle cannot.
|
|
12
|
+
export { createPGliteTestAdapter, type PGliteTestAdapter, type QueryLog } from "./test-utils/create-pglite-adapter.js";
|
|
10
13
|
export { createTestPluginContext } from "./test-utils/create-test-plugin-context.js";
|
|
11
14
|
export { createRepositoryTestHarness } from "./test-utils/create-repository-test-harness.js";
|
|
12
15
|
export { createPluginTestApp, type PluginTestApp, type TestAppEnv } from "./test-utils/create-plugin-test-app.js";
|