@porulle/core 0.10.8 → 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.
- package/dist/config/types.d.ts +3 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +4 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +37 -2
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +12 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1364 -401
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +86 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts +1 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +1 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.js +1 -0
- package/dist/modules/catalog/entity-service.d.ts +5 -3
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +73 -22
- package/dist/modules/catalog/module.d.ts +6 -1
- package/dist/modules/catalog/module.d.ts.map +1 -1
- package/dist/modules/catalog/module.js +6 -1
- package/dist/modules/catalog/ownership.d.ts +5 -0
- package/dist/modules/catalog/ownership.d.ts.map +1 -0
- package/dist/modules/catalog/ownership.js +11 -0
- package/dist/modules/catalog/repository/index.d.ts +52 -1
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +321 -2
- package/dist/modules/catalog/revision.d.ts +4 -0
- package/dist/modules/catalog/revision.d.ts.map +1 -0
- package/dist/modules/catalog/revision.js +33 -0
- package/dist/modules/catalog/schema.d.ts +979 -110
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +112 -2
- package/dist/modules/catalog/schemas.d.ts +2 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +2 -0
- package/dist/modules/catalog/service.d.ts +66 -3
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +512 -11
- package/dist/modules/channels/adapter.d.ts +85 -0
- package/dist/modules/channels/adapter.d.ts.map +1 -1
- package/dist/modules/channels/adapter.js +9 -1
- package/dist/modules/media/module.d.ts +68 -0
- package/dist/modules/media/module.d.ts.map +1 -1
- package/dist/modules/media/module.js +2 -0
- package/dist/modules/media/schema.d.ts +70 -0
- package/dist/modules/media/schema.d.ts.map +1 -1
- package/dist/modules/media/schema.js +20 -2
- package/dist/modules/media/service.d.ts +8 -1
- package/dist/modules/media/service.d.ts.map +1 -1
- package/dist/modules/media/service.js +66 -18
- package/dist/modules/pricing/schema.d.ts +17 -0
- package/dist/modules/pricing/schema.d.ts.map +1 -1
- package/dist/modules/pricing/schema.js +1 -0
- package/dist/modules/pricing/schemas.d.ts +1 -0
- package/dist/modules/pricing/schemas.d.ts.map +1 -1
- package/dist/modules/pricing/schemas.js +1 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +6 -1
- package/dist/modules/search/adapter.d.ts +2 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +2 -0
- package/dist/modules/search/service.d.ts +4 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +74 -5
- package/dist/runtime/kernel-modules.d.ts +73 -0
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config/types.ts +3 -0
- package/src/index.ts +14 -0
- package/src/interfaces/rest/index.ts +4 -0
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +37 -0
- package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
- package/src/interfaces/rest/routes/catalog.ts +85 -0
- package/src/interfaces/rest/routes/search.ts +16 -0
- package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
- package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
- package/src/interfaces/rest/schemas/catalog.ts +92 -0
- package/src/interfaces/rest/schemas/search.ts +2 -0
- package/src/modules/catalog/entity-service.ts +73 -20
- package/src/modules/catalog/module.ts +15 -0
- package/src/modules/catalog/ownership.ts +17 -0
- package/src/modules/catalog/repository/index.ts +547 -1
- package/src/modules/catalog/revision.ts +35 -0
- package/src/modules/catalog/schema.ts +169 -1
- package/src/modules/catalog/schemas.ts +2 -0
- package/src/modules/catalog/service.ts +738 -13
- package/src/modules/channels/adapter.ts +107 -1
- package/src/modules/media/module.ts +2 -0
- package/src/modules/media/schema.ts +23 -2
- package/src/modules/media/service.ts +84 -23
- package/src/modules/pricing/schema.ts +1 -0
- package/src/modules/pricing/schemas.ts +1 -0
- package/src/modules/pricing/service.ts +7 -2
- package/src/modules/search/adapter.ts +2 -0
- package/src/modules/search/module.ts +2 -0
- package/src/modules/search/service.ts +87 -6
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, and, inArray, type SQL } from "drizzle-orm";
|
|
1
|
+
import { eq, and, asc, count, desc, gt, inArray, isNull, lt, max, ne, or, type SQL } from "drizzle-orm";
|
|
2
2
|
import type { TxContext } from "../../../kernel/database/tx-context.js";
|
|
3
3
|
import { CommerceNotFoundError } from "../../../kernel/errors.js";
|
|
4
4
|
import type {
|
|
@@ -9,15 +9,21 @@ import {
|
|
|
9
9
|
sellableEntities,
|
|
10
10
|
sellableAttributes,
|
|
11
11
|
sellableCustomFields,
|
|
12
|
+
entityFieldDefinitions,
|
|
12
13
|
categories,
|
|
13
14
|
entityCategories,
|
|
14
15
|
brands,
|
|
15
16
|
entityBrands,
|
|
17
|
+
entityTags,
|
|
16
18
|
optionTypes,
|
|
17
19
|
optionValues,
|
|
18
20
|
variants,
|
|
19
21
|
variantOptionValues,
|
|
22
|
+
sellableEntityRevisions,
|
|
23
|
+
catalogFieldOwnership,
|
|
24
|
+
type SellableEntityRevisionSnapshot,
|
|
20
25
|
} from "../schema.js";
|
|
26
|
+
import { entityMedia } from "../../media/schema.js";
|
|
21
27
|
|
|
22
28
|
// Infer types from Drizzle schema
|
|
23
29
|
export type SellableEntity = typeof sellableEntities.$inferSelect;
|
|
@@ -27,6 +33,8 @@ export type SellableAttributeInsert = typeof sellableAttributes.$inferInsert;
|
|
|
27
33
|
export type SellableCustomField = typeof sellableCustomFields.$inferSelect;
|
|
28
34
|
export type SellableCustomFieldInsert =
|
|
29
35
|
typeof sellableCustomFields.$inferInsert;
|
|
36
|
+
export type EntityFieldDefinitionRecord = typeof entityFieldDefinitions.$inferSelect;
|
|
37
|
+
export type EntityFieldDefinitionInsert = typeof entityFieldDefinitions.$inferInsert;
|
|
30
38
|
export type Category = typeof categories.$inferSelect;
|
|
31
39
|
export type CategoryInsert = typeof categories.$inferInsert;
|
|
32
40
|
export type EntityCategory = typeof entityCategories.$inferSelect;
|
|
@@ -43,6 +51,14 @@ export type Variant = typeof variants.$inferSelect;
|
|
|
43
51
|
export type VariantInsert = typeof variants.$inferInsert;
|
|
44
52
|
export type VariantOptionValue = typeof variantOptionValues.$inferSelect;
|
|
45
53
|
export type VariantOptionValueInsert = typeof variantOptionValues.$inferInsert;
|
|
54
|
+
export type SellableEntityRevision = typeof sellableEntityRevisions.$inferSelect;
|
|
55
|
+
export type SellableEntityRevisionInsert = typeof sellableEntityRevisions.$inferInsert;
|
|
56
|
+
export type CatalogFieldOwnership = typeof catalogFieldOwnership.$inferSelect;
|
|
57
|
+
export type CatalogFieldOwnershipInsert = typeof catalogFieldOwnership.$inferInsert;
|
|
58
|
+
export type ProposedCustomField = SellableCustomField & {
|
|
59
|
+
entitySlug: string;
|
|
60
|
+
entityType: string;
|
|
61
|
+
};
|
|
46
62
|
|
|
47
63
|
/**
|
|
48
64
|
* CatalogRepository provides type-safe database operations for catalog entities.
|
|
@@ -165,6 +181,247 @@ export class CatalogRepository {
|
|
|
165
181
|
return result.length > 0;
|
|
166
182
|
}
|
|
167
183
|
|
|
184
|
+
async findFieldOwnership(
|
|
185
|
+
entityId: string,
|
|
186
|
+
ctx?: TxContext,
|
|
187
|
+
storeId?: string,
|
|
188
|
+
): Promise<CatalogFieldOwnership[]> {
|
|
189
|
+
const db = this.getDb(ctx);
|
|
190
|
+
const conditions = [eq(catalogFieldOwnership.entityId, entityId)];
|
|
191
|
+
if (storeId !== undefined) {
|
|
192
|
+
conditions.push(or(isNull(catalogFieldOwnership.storeId), eq(catalogFieldOwnership.storeId, storeId))!);
|
|
193
|
+
}
|
|
194
|
+
return db
|
|
195
|
+
.select()
|
|
196
|
+
.from(catalogFieldOwnership)
|
|
197
|
+
.where(and(...conditions))
|
|
198
|
+
.orderBy(asc(catalogFieldOwnership.fieldPath), asc(catalogFieldOwnership.updatedAt));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async findFieldOwnershipForResolution(
|
|
202
|
+
entityId: string,
|
|
203
|
+
storeId: string,
|
|
204
|
+
variantId: string | null,
|
|
205
|
+
ctx?: TxContext,
|
|
206
|
+
): Promise<CatalogFieldOwnership[]> {
|
|
207
|
+
const db = this.getDb(ctx);
|
|
208
|
+
return db
|
|
209
|
+
.select()
|
|
210
|
+
.from(catalogFieldOwnership)
|
|
211
|
+
.where(and(
|
|
212
|
+
eq(catalogFieldOwnership.entityId, entityId),
|
|
213
|
+
or(isNull(catalogFieldOwnership.storeId), eq(catalogFieldOwnership.storeId, storeId)),
|
|
214
|
+
variantId == null
|
|
215
|
+
? isNull(catalogFieldOwnership.variantId)
|
|
216
|
+
: or(isNull(catalogFieldOwnership.variantId), eq(catalogFieldOwnership.variantId, variantId)),
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async upsertFieldOwnership(
|
|
221
|
+
data: CatalogFieldOwnershipInsert,
|
|
222
|
+
ctx?: TxContext,
|
|
223
|
+
): Promise<CatalogFieldOwnership> {
|
|
224
|
+
const db = this.getDb(ctx);
|
|
225
|
+
const rows = await db
|
|
226
|
+
.insert(catalogFieldOwnership)
|
|
227
|
+
.values(data)
|
|
228
|
+
.onConflictDoUpdate({
|
|
229
|
+
target: [
|
|
230
|
+
catalogFieldOwnership.organizationId,
|
|
231
|
+
catalogFieldOwnership.entityId,
|
|
232
|
+
catalogFieldOwnership.variantId,
|
|
233
|
+
catalogFieldOwnership.storeId,
|
|
234
|
+
catalogFieldOwnership.fieldPath,
|
|
235
|
+
],
|
|
236
|
+
set: {
|
|
237
|
+
owner: data.owner,
|
|
238
|
+
updatedAt: data.updatedAt ?? new Date(),
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
.returning();
|
|
242
|
+
return rows[0]!;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async seedFieldOwnership(
|
|
246
|
+
data: CatalogFieldOwnershipInsert[],
|
|
247
|
+
ctx?: TxContext,
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
if (data.length === 0) return;
|
|
250
|
+
const db = this.getDb(ctx);
|
|
251
|
+
await db.insert(catalogFieldOwnership).values(data).onConflictDoNothing();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async findRevisionsByEntityId(
|
|
255
|
+
entityId: string,
|
|
256
|
+
ctx?: TxContext,
|
|
257
|
+
): Promise<SellableEntityRevision[]> {
|
|
258
|
+
const db = this.getDb(ctx);
|
|
259
|
+
return db
|
|
260
|
+
.select()
|
|
261
|
+
.from(sellableEntityRevisions)
|
|
262
|
+
.where(eq(sellableEntityRevisions.entityId, entityId))
|
|
263
|
+
.orderBy(asc(sellableEntityRevisions.revision));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async findRevisionMarkers(
|
|
267
|
+
entityId: string,
|
|
268
|
+
since?: Date,
|
|
269
|
+
ctx?: TxContext,
|
|
270
|
+
): Promise<Array<Pick<SellableEntityRevision, "createdAt" | "reason">>> {
|
|
271
|
+
const db = this.getDb(ctx);
|
|
272
|
+
return db
|
|
273
|
+
.select({
|
|
274
|
+
createdAt: sellableEntityRevisions.createdAt,
|
|
275
|
+
reason: sellableEntityRevisions.reason,
|
|
276
|
+
})
|
|
277
|
+
.from(sellableEntityRevisions)
|
|
278
|
+
.where(and(
|
|
279
|
+
eq(sellableEntityRevisions.entityId, entityId),
|
|
280
|
+
...(since ? [gt(sellableEntityRevisions.createdAt, since)] : []),
|
|
281
|
+
))
|
|
282
|
+
.orderBy(asc(sellableEntityRevisions.createdAt));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async findLatestRevision(
|
|
286
|
+
entityId: string,
|
|
287
|
+
ctx?: TxContext,
|
|
288
|
+
): Promise<SellableEntityRevision | undefined> {
|
|
289
|
+
const db = this.getDb(ctx);
|
|
290
|
+
const rows = await db
|
|
291
|
+
.select()
|
|
292
|
+
.from(sellableEntityRevisions)
|
|
293
|
+
.where(eq(sellableEntityRevisions.entityId, entityId))
|
|
294
|
+
.orderBy(desc(sellableEntityRevisions.revision))
|
|
295
|
+
.limit(1);
|
|
296
|
+
return rows[0];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async findRevisionById(
|
|
300
|
+
entityId: string,
|
|
301
|
+
id: string,
|
|
302
|
+
ctx?: TxContext,
|
|
303
|
+
): Promise<SellableEntityRevision | undefined> {
|
|
304
|
+
const db = this.getDb(ctx);
|
|
305
|
+
const rows = await db
|
|
306
|
+
.select()
|
|
307
|
+
.from(sellableEntityRevisions)
|
|
308
|
+
.where(
|
|
309
|
+
and(
|
|
310
|
+
eq(sellableEntityRevisions.entityId, entityId),
|
|
311
|
+
eq(sellableEntityRevisions.id, id),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
return rows[0];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async createRevision(
|
|
318
|
+
data: Omit<SellableEntityRevisionInsert, "id" | "revision" | "pinned">,
|
|
319
|
+
ctx?: TxContext,
|
|
320
|
+
): Promise<SellableEntityRevision> {
|
|
321
|
+
const db = this.getDb(ctx);
|
|
322
|
+
const rows = await db
|
|
323
|
+
.select({ revision: max(sellableEntityRevisions.revision) })
|
|
324
|
+
.from(sellableEntityRevisions)
|
|
325
|
+
.where(eq(sellableEntityRevisions.entityId, data.entityId));
|
|
326
|
+
const revision = (rows[0]?.revision ?? 0) + 1;
|
|
327
|
+
const inserted = await db
|
|
328
|
+
.insert(sellableEntityRevisions)
|
|
329
|
+
.values({
|
|
330
|
+
...data,
|
|
331
|
+
revision,
|
|
332
|
+
pinned: revision === 1,
|
|
333
|
+
})
|
|
334
|
+
.returning();
|
|
335
|
+
return inserted[0]!;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async updateRevision(
|
|
339
|
+
id: string,
|
|
340
|
+
data: Partial<Pick<SellableEntityRevisionInsert, "createdAt" | "pinned">>,
|
|
341
|
+
ctx?: TxContext,
|
|
342
|
+
): Promise<SellableEntityRevision | undefined> {
|
|
343
|
+
const db = this.getDb(ctx);
|
|
344
|
+
const rows = await db
|
|
345
|
+
.update(sellableEntityRevisions)
|
|
346
|
+
.set(data)
|
|
347
|
+
.where(eq(sellableEntityRevisions.id, id))
|
|
348
|
+
.returning();
|
|
349
|
+
return rows[0];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async deleteRevisionsOlderThan(
|
|
353
|
+
organizationId: string,
|
|
354
|
+
cutoff: Date,
|
|
355
|
+
ctx?: TxContext,
|
|
356
|
+
): Promise<number> {
|
|
357
|
+
const db = this.getDb(ctx);
|
|
358
|
+
const deleted = await db
|
|
359
|
+
.delete(sellableEntityRevisions)
|
|
360
|
+
.where(
|
|
361
|
+
and(
|
|
362
|
+
eq(sellableEntityRevisions.organizationId, organizationId),
|
|
363
|
+
lt(sellableEntityRevisions.createdAt, cutoff),
|
|
364
|
+
eq(sellableEntityRevisions.pinned, false),
|
|
365
|
+
gt(sellableEntityRevisions.revision, 1),
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
.returning({ id: sellableEntityRevisions.id });
|
|
369
|
+
return deleted.length;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async snapshotEntity(
|
|
373
|
+
entityId: string,
|
|
374
|
+
ctx?: TxContext,
|
|
375
|
+
): Promise<SellableEntityRevisionSnapshot> {
|
|
376
|
+
const entity = await this.findEntityById(entityId, ctx);
|
|
377
|
+
if (!entity) throw new CommerceNotFoundError("Entity not found.");
|
|
378
|
+
const [attributes, customFields, categoriesForEntity, brandsForEntity, media, tagsForEntity] = await Promise.all([
|
|
379
|
+
this.findAttributesByEntityId(entityId, ctx),
|
|
380
|
+
this.findAllCustomFieldsByEntityId(entityId, ctx),
|
|
381
|
+
this.findEntityCategories(entityId, ctx),
|
|
382
|
+
this.findEntityBrands(entityId, ctx),
|
|
383
|
+
this.findEntityMedia(entityId, ctx),
|
|
384
|
+
this.findEntityTags(entityId, ctx),
|
|
385
|
+
]);
|
|
386
|
+
return {
|
|
387
|
+
entity: entity as unknown as Record<string, unknown>,
|
|
388
|
+
attributes: attributes.sort((a, b) => a.locale.localeCompare(b.locale) || a.id.localeCompare(b.id)) as unknown as Array<Record<string, unknown>>,
|
|
389
|
+
customFields: customFields.sort((a, b) => a.locale.localeCompare(b.locale) || a.fieldName.localeCompare(b.fieldName) || a.id.localeCompare(b.id)) as unknown as Array<Record<string, unknown>>,
|
|
390
|
+
media: media.sort((a, b) => a.mediaAssetId.localeCompare(b.mediaAssetId) || (a.variantId ?? "").localeCompare(b.variantId ?? "")) as unknown as Array<Record<string, unknown>>,
|
|
391
|
+
categories: categoriesForEntity.sort((a, b) => a.categoryId.localeCompare(b.categoryId)) as unknown as Array<Record<string, unknown>>,
|
|
392
|
+
brands: brandsForEntity.sort((a, b) => a.brandId.localeCompare(b.brandId)) as unknown as Array<Record<string, unknown>>,
|
|
393
|
+
tags: tagsForEntity.sort((a, b) => a.tagId.localeCompare(b.tagId)) as unknown as Array<Record<string, unknown>>,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async findEntityMedia(
|
|
398
|
+
entityId: string,
|
|
399
|
+
ctx?: TxContext,
|
|
400
|
+
): Promise<Array<typeof entityMedia.$inferSelect>> {
|
|
401
|
+
const db = this.getDb(ctx);
|
|
402
|
+
return db
|
|
403
|
+
.select()
|
|
404
|
+
.from(entityMedia)
|
|
405
|
+
.where(eq(entityMedia.entityId, entityId));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async deleteEntityMediaByEntityId(
|
|
409
|
+
entityId: string,
|
|
410
|
+
ctx?: TxContext,
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
const db = this.getDb(ctx);
|
|
413
|
+
await db.delete(entityMedia).where(eq(entityMedia.entityId, entityId));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async createEntityMedia(
|
|
417
|
+
data: typeof entityMedia.$inferInsert,
|
|
418
|
+
ctx?: TxContext,
|
|
419
|
+
): Promise<typeof entityMedia.$inferSelect> {
|
|
420
|
+
const db = this.getDb(ctx);
|
|
421
|
+
const rows = await db.insert(entityMedia).values(data).returning();
|
|
422
|
+
return rows[0]!;
|
|
423
|
+
}
|
|
424
|
+
|
|
168
425
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
426
|
// Sellable Attributes
|
|
170
427
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -252,6 +509,22 @@ export class CatalogRepository {
|
|
|
252
509
|
async findCustomFieldsByEntityId(
|
|
253
510
|
entityId: string,
|
|
254
511
|
ctx?: TxContext,
|
|
512
|
+
): Promise<SellableCustomField[]> {
|
|
513
|
+
const db = this.getDb(ctx);
|
|
514
|
+
return db
|
|
515
|
+
.select()
|
|
516
|
+
.from(sellableCustomFields)
|
|
517
|
+
.where(
|
|
518
|
+
and(
|
|
519
|
+
eq(sellableCustomFields.entityId, entityId),
|
|
520
|
+
eq(sellableCustomFields.status, "approved"),
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async findAllCustomFieldsByEntityId(
|
|
526
|
+
entityId: string,
|
|
527
|
+
ctx?: TxContext,
|
|
255
528
|
): Promise<SellableCustomField[]> {
|
|
256
529
|
const db = this.getDb(ctx);
|
|
257
530
|
return db
|
|
@@ -260,6 +533,137 @@ export class CatalogRepository {
|
|
|
260
533
|
.where(eq(sellableCustomFields.entityId, entityId));
|
|
261
534
|
}
|
|
262
535
|
|
|
536
|
+
async findProposedCustomField(
|
|
537
|
+
entityId: string,
|
|
538
|
+
fieldName: string,
|
|
539
|
+
locale = "en",
|
|
540
|
+
ctx?: TxContext,
|
|
541
|
+
): Promise<SellableCustomField | undefined> {
|
|
542
|
+
const db = this.getDb(ctx);
|
|
543
|
+
const rows = await db
|
|
544
|
+
.select()
|
|
545
|
+
.from(sellableCustomFields)
|
|
546
|
+
.where(
|
|
547
|
+
and(
|
|
548
|
+
eq(sellableCustomFields.entityId, entityId),
|
|
549
|
+
eq(sellableCustomFields.fieldName, fieldName),
|
|
550
|
+
eq(sellableCustomFields.locale, locale),
|
|
551
|
+
eq(sellableCustomFields.status, "proposed"),
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
.orderBy(desc(sellableCustomFields.createdAt), desc(sellableCustomFields.id))
|
|
555
|
+
.limit(1);
|
|
556
|
+
return rows[0];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async updateProposedCustomField(
|
|
560
|
+
id: string,
|
|
561
|
+
data: Partial<Omit<SellableCustomFieldInsert, "id">>,
|
|
562
|
+
ctx?: TxContext,
|
|
563
|
+
): Promise<SellableCustomField | undefined> {
|
|
564
|
+
const db = this.getDb(ctx);
|
|
565
|
+
const rows = await db
|
|
566
|
+
.update(sellableCustomFields)
|
|
567
|
+
.set({ ...data, updatedAt: new Date() })
|
|
568
|
+
.where(
|
|
569
|
+
and(
|
|
570
|
+
eq(sellableCustomFields.id, id),
|
|
571
|
+
eq(sellableCustomFields.status, "proposed"),
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
.returning();
|
|
575
|
+
return rows[0];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async rejectOtherProposedCustomFields(
|
|
579
|
+
entityId: string,
|
|
580
|
+
fieldName: string,
|
|
581
|
+
locale: string,
|
|
582
|
+
excludeId: string,
|
|
583
|
+
ctx?: TxContext,
|
|
584
|
+
): Promise<number> {
|
|
585
|
+
const db = this.getDb(ctx);
|
|
586
|
+
const rows = await db
|
|
587
|
+
.update(sellableCustomFields)
|
|
588
|
+
.set({ status: "rejected", updatedAt: new Date() })
|
|
589
|
+
.where(
|
|
590
|
+
and(
|
|
591
|
+
eq(sellableCustomFields.entityId, entityId),
|
|
592
|
+
eq(sellableCustomFields.fieldName, fieldName),
|
|
593
|
+
eq(sellableCustomFields.locale, locale),
|
|
594
|
+
eq(sellableCustomFields.status, "proposed"),
|
|
595
|
+
ne(sellableCustomFields.id, excludeId),
|
|
596
|
+
),
|
|
597
|
+
)
|
|
598
|
+
.returning({ id: sellableCustomFields.id });
|
|
599
|
+
return rows.length;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async listProposedCustomFields(
|
|
603
|
+
organizationId: string,
|
|
604
|
+
pagination: { page: number; limit: number },
|
|
605
|
+
filter?: { entityType?: string },
|
|
606
|
+
ctx?: TxContext,
|
|
607
|
+
): Promise<{ items: ProposedCustomField[]; total: number }> {
|
|
608
|
+
const db = this.getDb(ctx);
|
|
609
|
+
const page = Math.max(1, pagination.page);
|
|
610
|
+
const limit = Math.max(1, pagination.limit);
|
|
611
|
+
const conditions: SQL[] = [
|
|
612
|
+
eq(sellableCustomFields.status, "proposed"),
|
|
613
|
+
eq(sellableEntities.organizationId, organizationId),
|
|
614
|
+
];
|
|
615
|
+
if (filter?.entityType !== undefined) {
|
|
616
|
+
conditions.push(eq(sellableEntities.type, filter.entityType));
|
|
617
|
+
}
|
|
618
|
+
const where = and(...conditions);
|
|
619
|
+
const rows = await db
|
|
620
|
+
.select({
|
|
621
|
+
customField: sellableCustomFields,
|
|
622
|
+
entitySlug: sellableEntities.slug,
|
|
623
|
+
entityType: sellableEntities.type,
|
|
624
|
+
})
|
|
625
|
+
.from(sellableCustomFields)
|
|
626
|
+
.innerJoin(sellableEntities, eq(sellableCustomFields.entityId, sellableEntities.id))
|
|
627
|
+
.where(where)
|
|
628
|
+
.orderBy(asc(sellableCustomFields.createdAt), asc(sellableCustomFields.id))
|
|
629
|
+
.limit(limit)
|
|
630
|
+
.offset((page - 1) * limit);
|
|
631
|
+
const totalRows = await db
|
|
632
|
+
.select({ count: count() })
|
|
633
|
+
.from(sellableCustomFields)
|
|
634
|
+
.innerJoin(sellableEntities, eq(sellableCustomFields.entityId, sellableEntities.id))
|
|
635
|
+
.where(where);
|
|
636
|
+
return {
|
|
637
|
+
items: rows.map(({ customField, entitySlug, entityType }) => ({
|
|
638
|
+
...customField,
|
|
639
|
+
entitySlug,
|
|
640
|
+
entityType,
|
|
641
|
+
})),
|
|
642
|
+
total: totalRows[0]?.count ?? 0,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async findCustomFieldByName(
|
|
647
|
+
entityId: string,
|
|
648
|
+
fieldName: string,
|
|
649
|
+
locale = "en",
|
|
650
|
+
ctx?: TxContext,
|
|
651
|
+
): Promise<SellableCustomField | undefined> {
|
|
652
|
+
const db = this.getDb(ctx);
|
|
653
|
+
const rows = await db
|
|
654
|
+
.select()
|
|
655
|
+
.from(sellableCustomFields)
|
|
656
|
+
.where(
|
|
657
|
+
and(
|
|
658
|
+
eq(sellableCustomFields.entityId, entityId),
|
|
659
|
+
eq(sellableCustomFields.fieldName, fieldName),
|
|
660
|
+
eq(sellableCustomFields.locale, locale),
|
|
661
|
+
eq(sellableCustomFields.status, "approved"),
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
return rows[0];
|
|
665
|
+
}
|
|
666
|
+
|
|
263
667
|
async createCustomField(
|
|
264
668
|
data: SellableCustomFieldInsert,
|
|
265
669
|
ctx?: TxContext,
|
|
@@ -269,6 +673,54 @@ export class CatalogRepository {
|
|
|
269
673
|
return rows[0]!;
|
|
270
674
|
}
|
|
271
675
|
|
|
676
|
+
async updateCustomField(
|
|
677
|
+
id: string,
|
|
678
|
+
data: Partial<Omit<SellableCustomFieldInsert, "id">>,
|
|
679
|
+
ctx?: TxContext,
|
|
680
|
+
): Promise<SellableCustomField | undefined> {
|
|
681
|
+
const db = this.getDb(ctx);
|
|
682
|
+
const rows = await db
|
|
683
|
+
.update(sellableCustomFields)
|
|
684
|
+
.set({ ...data, updatedAt: new Date() })
|
|
685
|
+
.where(eq(sellableCustomFields.id, id))
|
|
686
|
+
.returning();
|
|
687
|
+
return rows[0];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async upsertCustomField(
|
|
691
|
+
entityId: string,
|
|
692
|
+
fieldName: string,
|
|
693
|
+
locale: string,
|
|
694
|
+
data: Omit<SellableCustomFieldInsert, "entityId" | "fieldName">,
|
|
695
|
+
ctx?: TxContext,
|
|
696
|
+
): Promise<SellableCustomField> {
|
|
697
|
+
const existing = await this.findCustomFieldByName(entityId, fieldName, locale, ctx);
|
|
698
|
+
if (existing) {
|
|
699
|
+
const updated = await this.updateCustomField(existing.id, data, ctx);
|
|
700
|
+
return updated!;
|
|
701
|
+
}
|
|
702
|
+
return this.createCustomField({ ...data, entityId, fieldName, locale }, ctx);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async deleteCustomField(
|
|
706
|
+
entityId: string,
|
|
707
|
+
fieldName: string,
|
|
708
|
+
locale = "en",
|
|
709
|
+
ctx?: TxContext,
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
const db = this.getDb(ctx);
|
|
712
|
+
await db
|
|
713
|
+
.delete(sellableCustomFields)
|
|
714
|
+
.where(
|
|
715
|
+
and(
|
|
716
|
+
eq(sellableCustomFields.entityId, entityId),
|
|
717
|
+
eq(sellableCustomFields.fieldName, fieldName),
|
|
718
|
+
eq(sellableCustomFields.locale, locale),
|
|
719
|
+
eq(sellableCustomFields.status, "approved"),
|
|
720
|
+
),
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
272
724
|
async deleteCustomFieldsByEntityId(
|
|
273
725
|
entityId: string,
|
|
274
726
|
ctx?: TxContext,
|
|
@@ -279,6 +731,79 @@ export class CatalogRepository {
|
|
|
279
731
|
.where(eq(sellableCustomFields.entityId, entityId));
|
|
280
732
|
}
|
|
281
733
|
|
|
734
|
+
async findEntityFieldDefinitions(
|
|
735
|
+
organizationId: string,
|
|
736
|
+
entityType?: string,
|
|
737
|
+
ctx?: TxContext,
|
|
738
|
+
): Promise<EntityFieldDefinitionRecord[]> {
|
|
739
|
+
const db = this.getDb(ctx);
|
|
740
|
+
const conditions = [eq(entityFieldDefinitions.organizationId, organizationId)];
|
|
741
|
+
if (entityType !== undefined) {
|
|
742
|
+
conditions.push(eq(entityFieldDefinitions.entityType, entityType));
|
|
743
|
+
}
|
|
744
|
+
return db
|
|
745
|
+
.select()
|
|
746
|
+
.from(entityFieldDefinitions)
|
|
747
|
+
.where(and(...conditions))
|
|
748
|
+
.orderBy(asc(entityFieldDefinitions.entityType), asc(entityFieldDefinitions.sortOrder), asc(entityFieldDefinitions.name));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async findActiveEntityFieldDefinitions(
|
|
752
|
+
organizationId: string,
|
|
753
|
+
entityType: string,
|
|
754
|
+
ctx?: TxContext,
|
|
755
|
+
): Promise<EntityFieldDefinitionRecord[]> {
|
|
756
|
+
const db = this.getDb(ctx);
|
|
757
|
+
return db
|
|
758
|
+
.select()
|
|
759
|
+
.from(entityFieldDefinitions)
|
|
760
|
+
.where(
|
|
761
|
+
and(
|
|
762
|
+
eq(entityFieldDefinitions.organizationId, organizationId),
|
|
763
|
+
eq(entityFieldDefinitions.entityType, entityType),
|
|
764
|
+
eq(entityFieldDefinitions.status, "active"),
|
|
765
|
+
),
|
|
766
|
+
)
|
|
767
|
+
.orderBy(asc(entityFieldDefinitions.sortOrder), asc(entityFieldDefinitions.name));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async findEntityFieldDefinitionById(
|
|
771
|
+
organizationId: string,
|
|
772
|
+
id: string,
|
|
773
|
+
ctx?: TxContext,
|
|
774
|
+
): Promise<EntityFieldDefinitionRecord | undefined> {
|
|
775
|
+
const db = this.getDb(ctx);
|
|
776
|
+
const rows = await db
|
|
777
|
+
.select()
|
|
778
|
+
.from(entityFieldDefinitions)
|
|
779
|
+
.where(and(eq(entityFieldDefinitions.organizationId, organizationId), eq(entityFieldDefinitions.id, id)));
|
|
780
|
+
return rows[0];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async createEntityFieldDefinition(
|
|
784
|
+
data: EntityFieldDefinitionInsert,
|
|
785
|
+
ctx?: TxContext,
|
|
786
|
+
): Promise<EntityFieldDefinitionRecord> {
|
|
787
|
+
const db = this.getDb(ctx);
|
|
788
|
+
const rows = await db.insert(entityFieldDefinitions).values(data).returning();
|
|
789
|
+
return rows[0]!;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async updateEntityFieldDefinition(
|
|
793
|
+
organizationId: string,
|
|
794
|
+
id: string,
|
|
795
|
+
data: Partial<Omit<EntityFieldDefinitionInsert, "id" | "organizationId">>,
|
|
796
|
+
ctx?: TxContext,
|
|
797
|
+
): Promise<EntityFieldDefinitionRecord | undefined> {
|
|
798
|
+
const db = this.getDb(ctx);
|
|
799
|
+
const rows = await db
|
|
800
|
+
.update(entityFieldDefinitions)
|
|
801
|
+
.set({ ...data, updatedAt: new Date() })
|
|
802
|
+
.where(and(eq(entityFieldDefinitions.organizationId, organizationId), eq(entityFieldDefinitions.id, id)))
|
|
803
|
+
.returning();
|
|
804
|
+
return rows[0];
|
|
805
|
+
}
|
|
806
|
+
|
|
282
807
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
283
808
|
// Categories
|
|
284
809
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -511,6 +1036,27 @@ export class CatalogRepository {
|
|
|
511
1036
|
.where(eq(entityBrands.entityId, entityId));
|
|
512
1037
|
}
|
|
513
1038
|
|
|
1039
|
+
async findEntityTags(
|
|
1040
|
+
entityId: string,
|
|
1041
|
+
ctx?: TxContext,
|
|
1042
|
+
): Promise<Array<typeof entityTags.$inferSelect>> {
|
|
1043
|
+
const db = this.getDb(ctx);
|
|
1044
|
+
return db
|
|
1045
|
+
.select()
|
|
1046
|
+
.from(entityTags)
|
|
1047
|
+
.where(eq(entityTags.entityId, entityId));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async addEntityTag(entityId: string, tagId: string, ctx?: TxContext): Promise<void> {
|
|
1051
|
+
const db = this.getDb(ctx);
|
|
1052
|
+
await db.insert(entityTags).values({ entityId, tagId }).onConflictDoNothing();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async deleteEntityTagsByEntityId(entityId: string, ctx?: TxContext): Promise<void> {
|
|
1056
|
+
const db = this.getDb(ctx);
|
|
1057
|
+
await db.delete(entityTags).where(eq(entityTags.entityId, entityId));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
514
1060
|
async addEntityToBrand(
|
|
515
1061
|
entityId: string,
|
|
516
1062
|
brandId: string,
|