@porulle/core 0.10.6 → 0.11.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.
Files changed (136) hide show
  1. package/dist/config/types.d.ts +3 -0
  2. package/dist/config/types.d.ts.map +1 -1
  3. package/dist/hooks/checkout.d.ts +2 -0
  4. package/dist/hooks/checkout.d.ts.map +1 -1
  5. package/dist/hooks/checkout.js +2 -0
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/interfaces/rest/index.d.ts.map +1 -1
  10. package/dist/interfaces/rest/index.js +4 -0
  11. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts +5 -0
  12. package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
  13. package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
  14. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
  15. package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
  16. package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
  17. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  18. package/dist/interfaces/rest/routes/catalog.js +37 -2
  19. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  20. package/dist/interfaces/rest/routes/checkout.js +7 -1
  21. package/dist/interfaces/rest/routes/payments.d.ts.map +1 -1
  22. package/dist/interfaces/rest/routes/payments.js +48 -30
  23. package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
  24. package/dist/interfaces/rest/routes/search.js +12 -0
  25. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
  26. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
  27. package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
  28. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
  29. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
  30. package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
  31. package/dist/interfaces/rest/schemas/catalog.d.ts +1364 -401
  32. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  33. package/dist/interfaces/rest/schemas/catalog.js +86 -0
  34. package/dist/interfaces/rest/schemas/pricing.d.ts +1 -0
  35. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  36. package/dist/interfaces/rest/schemas/search.d.ts +1 -0
  37. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  38. package/dist/interfaces/rest/schemas/search.js +1 -0
  39. package/dist/kernel/error-mapper.d.ts.map +1 -1
  40. package/dist/kernel/error-mapper.js +3 -0
  41. package/dist/modules/catalog/entity-service.d.ts +5 -3
  42. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  43. package/dist/modules/catalog/entity-service.js +73 -22
  44. package/dist/modules/catalog/module.d.ts +6 -1
  45. package/dist/modules/catalog/module.d.ts.map +1 -1
  46. package/dist/modules/catalog/module.js +6 -1
  47. package/dist/modules/catalog/ownership.d.ts +5 -0
  48. package/dist/modules/catalog/ownership.d.ts.map +1 -0
  49. package/dist/modules/catalog/ownership.js +11 -0
  50. package/dist/modules/catalog/repository/index.d.ts +52 -1
  51. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  52. package/dist/modules/catalog/repository/index.js +321 -2
  53. package/dist/modules/catalog/revision.d.ts +4 -0
  54. package/dist/modules/catalog/revision.d.ts.map +1 -0
  55. package/dist/modules/catalog/revision.js +33 -0
  56. package/dist/modules/catalog/schema.d.ts +979 -110
  57. package/dist/modules/catalog/schema.d.ts.map +1 -1
  58. package/dist/modules/catalog/schema.js +112 -2
  59. package/dist/modules/catalog/schemas.d.ts +2 -0
  60. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  61. package/dist/modules/catalog/schemas.js +2 -0
  62. package/dist/modules/catalog/service.d.ts +66 -3
  63. package/dist/modules/catalog/service.d.ts.map +1 -1
  64. package/dist/modules/catalog/service.js +512 -11
  65. package/dist/modules/channels/adapter.d.ts +85 -0
  66. package/dist/modules/channels/adapter.d.ts.map +1 -1
  67. package/dist/modules/channels/adapter.js +9 -1
  68. package/dist/modules/media/module.d.ts +68 -0
  69. package/dist/modules/media/module.d.ts.map +1 -1
  70. package/dist/modules/media/module.js +2 -0
  71. package/dist/modules/media/schema.d.ts +70 -0
  72. package/dist/modules/media/schema.d.ts.map +1 -1
  73. package/dist/modules/media/schema.js +20 -2
  74. package/dist/modules/media/service.d.ts +8 -1
  75. package/dist/modules/media/service.d.ts.map +1 -1
  76. package/dist/modules/media/service.js +66 -18
  77. package/dist/modules/orders/service.d.ts +2 -0
  78. package/dist/modules/orders/service.d.ts.map +1 -1
  79. package/dist/modules/orders/service.js +1 -0
  80. package/dist/modules/pricing/schema.d.ts +17 -0
  81. package/dist/modules/pricing/schema.d.ts.map +1 -1
  82. package/dist/modules/pricing/schema.js +1 -0
  83. package/dist/modules/pricing/schemas.d.ts +1 -0
  84. package/dist/modules/pricing/schemas.d.ts.map +1 -1
  85. package/dist/modules/pricing/schemas.js +1 -0
  86. package/dist/modules/pricing/service.d.ts.map +1 -1
  87. package/dist/modules/pricing/service.js +6 -1
  88. package/dist/modules/search/adapter.d.ts +2 -0
  89. package/dist/modules/search/adapter.d.ts.map +1 -1
  90. package/dist/modules/search/module.d.ts.map +1 -1
  91. package/dist/modules/search/module.js +2 -0
  92. package/dist/modules/search/service.d.ts +4 -0
  93. package/dist/modules/search/service.d.ts.map +1 -1
  94. package/dist/modules/search/service.js +74 -5
  95. package/dist/runtime/kernel-modules.d.ts +73 -0
  96. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  97. package/dist/utils/id.d.ts +1 -0
  98. package/dist/utils/id.d.ts.map +1 -1
  99. package/dist/utils/id.js +8 -0
  100. package/package.json +1 -1
  101. package/src/config/types.ts +3 -0
  102. package/src/hooks/checkout.ts +4 -0
  103. package/src/index.ts +14 -0
  104. package/src/interfaces/rest/index.ts +4 -0
  105. package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +37 -0
  106. package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
  107. package/src/interfaces/rest/routes/catalog.ts +85 -0
  108. package/src/interfaces/rest/routes/checkout.ts +7 -1
  109. package/src/interfaces/rest/routes/payments.ts +55 -35
  110. package/src/interfaces/rest/routes/search.ts +16 -0
  111. package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
  112. package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
  113. package/src/interfaces/rest/schemas/catalog.ts +92 -0
  114. package/src/interfaces/rest/schemas/search.ts +2 -0
  115. package/src/kernel/error-mapper.ts +3 -0
  116. package/src/modules/catalog/entity-service.ts +73 -20
  117. package/src/modules/catalog/module.ts +15 -0
  118. package/src/modules/catalog/ownership.ts +17 -0
  119. package/src/modules/catalog/repository/index.ts +547 -1
  120. package/src/modules/catalog/revision.ts +35 -0
  121. package/src/modules/catalog/schema.ts +169 -1
  122. package/src/modules/catalog/schemas.ts +2 -0
  123. package/src/modules/catalog/service.ts +738 -13
  124. package/src/modules/channels/adapter.ts +107 -1
  125. package/src/modules/media/module.ts +2 -0
  126. package/src/modules/media/schema.ts +23 -2
  127. package/src/modules/media/service.ts +84 -23
  128. package/src/modules/orders/service.ts +3 -0
  129. package/src/modules/pricing/schema.ts +1 -0
  130. package/src/modules/pricing/schemas.ts +1 -0
  131. package/src/modules/pricing/service.ts +7 -2
  132. package/src/modules/search/adapter.ts +2 -0
  133. package/src/modules/search/module.ts +2 -0
  134. package/src/modules/search/service.ts +87 -6
  135. package/src/utils/id.ts +11 -0
  136. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -36,6 +36,7 @@ import type {
36
36
  CreateOptionTypeInput,
37
37
  CreateOptionValueInput,
38
38
  CreateVariantInput,
39
+ EntityFieldDefinitionResolver,
39
40
  } from "./service.js";
40
41
  import type {
41
42
  SellableEntity,
@@ -69,6 +70,7 @@ function getCustomFieldValue(field: SellableCustomField): unknown {
69
70
  switch (field.fieldType) {
70
71
  case "text":
71
72
  case "relation":
73
+ case "select":
72
74
  return field.textValue;
73
75
  case "number":
74
76
  return field.numberValue;
@@ -83,8 +85,14 @@ function getCustomFieldValue(field: SellableCustomField): unknown {
83
85
  }
84
86
  }
85
87
 
88
+ type CustomFieldData = Omit<SellableCustomFieldInsert, "entityId" | "fieldName" | "locale">;
89
+ type ValidatedCustomField = { fieldName: string; locale: string; data: CustomFieldData | null };
90
+
86
91
  export class EntityService {
87
- constructor(private readonly deps: CatalogServiceDeps) {}
92
+ constructor(
93
+ private readonly deps: CatalogServiceDeps,
94
+ private readonly resolveEntityFieldDefinitions: EntityFieldDefinitionResolver,
95
+ ) {}
88
96
 
89
97
  private get repo() {
90
98
  return this.deps.repository;
@@ -102,22 +110,38 @@ export class EntityService {
102
110
  if (entity.sourceStoreId != null) assertPermission(actor, "catalog:sync");
103
111
  }
104
112
 
105
- private async validateAndCreateCustomFields(entityId: string, entityType: string, customFields: Record<string, unknown> | undefined, ctx?: TxContext): Promise<Result<void>> {
106
- if (!customFields) return Ok(undefined);
113
+ private async validateCustomFields(entityType: string, customFields: Record<string, unknown> | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<ValidatedCustomField[]>> {
114
+ if (!customFields) return Ok([]);
107
115
  const entityConfig = this.deps.config.entities?.[entityType];
108
- if (!entityConfig) return Ok(undefined);
109
- const definitionMap = new Map(entityConfig.fields.map((f) => [f.name, f]));
116
+ const definitions = await this.resolveEntityFieldDefinitions(entityType, actor ?? ctx?.actor ?? null, ctx);
117
+ if (!entityConfig && definitions.length === 0) return Ok([]);
118
+ const definitionMap = new Map(definitions.map((f) => [f.name, f]));
119
+ const validated: ValidatedCustomField[] = [];
110
120
  for (const [name, value] of Object.entries(customFields)) {
111
121
  const def = definitionMap.get(name);
112
122
  if (!def) return Err(new CommerceValidationError(`Unknown custom field: ${name}`));
123
+ if (value === null) {
124
+ validated.push({ fieldName: name, locale: "en", data: null });
125
+ continue;
126
+ }
113
127
  const type = def.type;
128
+ let normalized = value;
114
129
  let valid = false;
115
130
  switch (type) {
116
131
  case "text":
117
132
  case "relation":
118
- case "select":
119
133
  valid = typeof value === "string";
120
134
  break;
135
+ case "select": {
136
+ if (typeof value !== "string") break;
137
+ const trimmed = value.trim();
138
+ if (def.options && !def.options.includes(trimmed)) {
139
+ return Err(new CommerceValidationError(`Value "${trimmed}" is not permitted for field ${name}. Allowed: ${def.options.join(", ")}`));
140
+ }
141
+ normalized = trimmed;
142
+ valid = true;
143
+ break;
144
+ }
121
145
  case "number":
122
146
  valid = typeof value === "number";
123
147
  break;
@@ -134,29 +158,50 @@ export class EntityService {
134
158
  valid = false;
135
159
  }
136
160
  if (!valid) return Err(new CommerceValidationError(`Custom field ${name} expected type ${type}.`));
137
- const fieldType = (type === "select" ? "text" : type) as SellableCustomField["fieldType"];
138
- const insertData: SellableCustomFieldInsert = { entityId, fieldName: name, fieldType };
161
+ const fieldType = type as SellableCustomField["fieldType"];
162
+ const insertData: CustomFieldData = {
163
+ fieldType,
164
+ source: "merchant",
165
+ status: "approved",
166
+ confidence: null,
167
+ evidence: null,
168
+ approvedAt: null,
169
+ approvedBy: null,
170
+ };
139
171
  switch (fieldType) {
140
172
  case "text":
141
173
  case "relation":
142
- insertData.textValue = value as string;
174
+ case "select":
175
+ insertData.textValue = normalized as string;
143
176
  break;
144
177
  case "number":
145
- insertData.numberValue = value as number;
178
+ insertData.numberValue = normalized as number;
146
179
  break;
147
180
  case "boolean":
148
- insertData.booleanValue = value as boolean;
181
+ insertData.booleanValue = normalized as boolean;
149
182
  break;
150
183
  case "date":
151
- insertData.dateValue = value instanceof Date ? value : new Date(value as string);
184
+ insertData.dateValue = normalized instanceof Date ? normalized : new Date(normalized as string);
152
185
  break;
153
186
  case "json":
154
- insertData.jsonValue = value;
187
+ insertData.jsonValue = normalized;
155
188
  break;
156
189
  }
157
- await this.repo.createCustomField(insertData, ctx);
190
+ validated.push({ fieldName: name, locale: "en", data: insertData });
191
+ }
192
+ return Ok(validated);
193
+ }
194
+
195
+ private async writeCustomFields(entityId: string, customFields: ValidatedCustomField[], ctx?: TxContext, upsert = false): Promise<void> {
196
+ for (const customField of customFields) {
197
+ if (customField.data === null) {
198
+ await this.repo.deleteCustomField(entityId, customField.fieldName, customField.locale, ctx);
199
+ } else if (upsert) {
200
+ await this.repo.upsertCustomField(entityId, customField.fieldName, customField.locale, customField.data, ctx);
201
+ } else {
202
+ await this.repo.createCustomField({ ...customField.data, entityId, fieldName: customField.fieldName, locale: customField.locale }, ctx);
203
+ }
158
204
  }
159
- return Ok(undefined);
160
205
  }
161
206
 
162
207
  private async hydrateEntity(entity: SellableEntity, options?: GetOptions, ctx?: TxContext): Promise<CatalogEntityHydrated> {
@@ -185,14 +230,16 @@ export class EntityService {
185
230
  }
186
231
  if (options?.includeCategories) hydrated.categories = await this.repo.findEntityCategories(entity.id, ctx);
187
232
  if (options?.includeBrands) hydrated.brands = await this.repo.findEntityBrands(entity.id, ctx);
233
+ if (options?.includeCustomFields) hydrated.customFields = await this.repo.findCustomFieldsByEntityId(entity.id, ctx);
188
234
  if (options?.includeMedia) {
189
235
  const mediaService = this.deps.services.media as {
190
236
  listEntityMedia?: (
191
237
  entityId: string,
192
238
  opts?: { orgId?: string },
239
+ ctx?: TxContext,
193
240
  ) => Promise<{ ok: boolean; value?: CatalogEntityHydrated["media"] }>;
194
241
  } | undefined;
195
- const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId });
242
+ const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId }, ctx);
196
243
  hydrated.media = mediaResult?.ok && mediaResult.value ? mediaResult.value : [];
197
244
  }
198
245
  if (options?.includePricing) {
@@ -217,12 +264,15 @@ export class EntityService {
217
264
  const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
218
265
  const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
219
266
  if (processedInput.sourceStoreId != null) assertPermission(actor, "catalog:sync");
220
- const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, ...(processedInput.sourceStoreId !== undefined ? { sourceStoreId: processedInput.sourceStoreId } : {}), ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
267
+ const customFieldsResult = await this.validateCustomFields(processedInput.type, processedInput.customFields, actor, ctx);
268
+ if (!customFieldsResult.ok) return customFieldsResult;
269
+ const statuses = ["draft", "active", "archived", "discontinued"] as const;
270
+ const status = statuses.find((value) => value === processedInput.status) ?? "draft";
271
+ const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status, isVisible: status === "active", ...(processedInput.sourceStoreId !== undefined ? { sourceStoreId: processedInput.sourceStoreId } : {}), ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
221
272
  if (processedInput.attributes) {
222
273
  await this.repo.createAttribute({ entityId: entity.id, locale: processedInput.attributes.locale ?? "en", title: processedInput.attributes.title, subtitle: processedInput.attributes.subtitle, description: processedInput.attributes.description, richDescription: processedInput.attributes.richDescription, seoTitle: processedInput.attributes.seoTitle, seoDescription: processedInput.attributes.seoDescription }, ctx);
223
274
  }
224
- const customFieldsResult = await this.validateAndCreateCustomFields(entity.id, entity.type, processedInput.customFields, ctx);
225
- if (!customFieldsResult.ok) return customFieldsResult;
275
+ await this.writeCustomFields(entity.id, customFieldsResult.value, ctx);
226
276
  const hookReport = await runAfterHooks(afterHooks, null, entity, "create", context);
227
277
  const hydrated = await this.hydrateEntity(entity, undefined, ctx);
228
278
  return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
@@ -238,8 +288,11 @@ export class EntityService {
238
288
  const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
239
289
  const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.update"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
240
290
  const processed = await runBeforeHooks(beforeHooks, input, "update", context);
291
+ const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, ctx);
292
+ if (!customFieldsResult.ok) return customFieldsResult;
241
293
  const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, ctx);
242
294
  if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
295
+ await this.writeCustomFields(existing.id, customFieldsResult.value, ctx, true);
243
296
  const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
244
297
  const hydrated = await this.hydrateEntity(updated, undefined, ctx);
245
298
  return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
@@ -467,7 +520,7 @@ export class EntityService {
467
520
  if (!ov) return Err(new CommerceValidationError(`Option value "${optVal}" does not exist for option type "${optName}".`));
468
521
  optionValueIds.push(ov.id);
469
522
  }
470
- const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
523
+ const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.barcode !== undefined ? { barcode: input.barcode } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
471
524
  await this.repo.createVariantOptionValues(optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })), ctx);
472
525
  return Ok(variant);
473
526
  }
@@ -6,15 +6,20 @@ import { CatalogRepository } from "./repository/index.js";
6
6
  import {
7
7
  brands,
8
8
  categories,
9
+ tags,
10
+ entityTags,
9
11
  entityBrands,
10
12
  entityCategories,
11
13
  optionTypes,
12
14
  optionValues,
13
15
  sellableAttributes,
14
16
  sellableCustomFields,
17
+ entityFieldDefinitions,
18
+ sellableEntityRevisions,
15
19
  sellableEntities,
16
20
  variantOptionValues,
17
21
  variants,
22
+ catalogFieldOwnership,
18
23
  } from "./schema.js";
19
24
  import { CatalogServiceImpl } from "./service.js";
20
25
 
@@ -27,14 +32,19 @@ export const catalogModule = defineModule<
27
32
  sellableEntities: typeof sellableEntities;
28
33
  sellableAttributes: typeof sellableAttributes;
29
34
  sellableCustomFields: typeof sellableCustomFields;
35
+ entityFieldDefinitions: typeof entityFieldDefinitions;
36
+ sellableEntityRevisions: typeof sellableEntityRevisions;
30
37
  categories: typeof categories;
31
38
  entityCategories: typeof entityCategories;
32
39
  brands: typeof brands;
33
40
  entityBrands: typeof entityBrands;
41
+ tags: typeof tags;
42
+ entityTags: typeof entityTags;
34
43
  optionTypes: typeof optionTypes;
35
44
  optionValues: typeof optionValues;
36
45
  variants: typeof variants;
37
46
  variantOptionValues: typeof variantOptionValues;
47
+ catalogFieldOwnership: typeof catalogFieldOwnership;
38
48
  },
39
49
  CatalogServiceImpl,
40
50
  CatalogModuleDeps
@@ -45,14 +55,19 @@ export const catalogModule = defineModule<
45
55
  sellableEntities,
46
56
  sellableAttributes,
47
57
  sellableCustomFields,
58
+ entityFieldDefinitions,
59
+ sellableEntityRevisions,
48
60
  categories,
49
61
  entityCategories,
50
62
  brands,
51
63
  entityBrands,
64
+ tags,
65
+ entityTags,
52
66
  optionTypes,
53
67
  optionValues,
54
68
  variants,
55
69
  variantOptionValues,
70
+ catalogFieldOwnership,
56
71
  }),
57
72
  service: (deps) =>
58
73
  new CatalogServiceImpl({
@@ -0,0 +1,17 @@
1
+ import { CommerceValidationError } from "../../kernel/errors.js";
2
+
3
+ export type FieldPath = string;
4
+ export type FieldOwner = "platform" | "store" | "shared";
5
+
6
+ const fieldPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
7
+
8
+ export function isValidFieldPath(value: string): value is FieldPath {
9
+ return fieldPathPattern.test(value);
10
+ }
11
+
12
+ export function validateFieldPath(value: string): FieldPath {
13
+ if (!isValidFieldPath(value)) {
14
+ throw new CommerceValidationError("Field path must contain dot-separated alphanumeric, underscore, or hyphen segments.");
15
+ }
16
+ return value;
17
+ }