includio-cms 0.26.0 → 0.27.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 (112) hide show
  1. package/API.md +42 -2
  2. package/CHANGELOG.md +65 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +8 -0
  5. package/dist/admin/auth-client.d.ts +42 -42
  6. package/dist/admin/client/admin/admin-layout.svelte +12 -2
  7. package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
  8. package/dist/admin/client/collection/data-table.svelte +0 -39
  9. package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
  10. package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
  11. package/dist/admin/client/shop/refund-dialog.svelte +37 -1
  12. package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
  13. package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -0
  14. package/dist/admin/components/fields/field-renderer.svelte +6 -1
  15. package/dist/admin/components/fields/icon-field.svelte +86 -0
  16. package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
  17. package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
  18. package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
  19. package/dist/admin/components/fields/object-field.svelte +27 -7
  20. package/dist/admin/components/fields/shop-field.svelte +210 -20
  21. package/dist/admin/components/layout/layout-tabs.svelte +1 -0
  22. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
  23. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
  24. package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
  25. package/dist/admin/helpers/build-icon-set-map.js +16 -0
  26. package/dist/admin/helpers/index.d.ts +2 -0
  27. package/dist/admin/helpers/index.js +2 -0
  28. package/dist/admin/remote/shop.remote.d.ts +58 -24
  29. package/dist/admin/remote/shop.remote.js +61 -6
  30. package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
  31. package/dist/admin/state/icon-sets.svelte.js +20 -0
  32. package/dist/cli/scaffold/admin.js +2 -2
  33. package/dist/components/ui/checkbox/checkbox.svelte +3 -3
  34. package/dist/core/cms.d.ts +11 -2
  35. package/dist/core/cms.js +29 -0
  36. package/dist/core/fields/fieldSchemaToTs.js +7 -0
  37. package/dist/core/server/generator/fields.d.ts +2 -0
  38. package/dist/core/server/generator/fields.js +34 -1
  39. package/dist/core/server/generator/generator.js +2 -1
  40. package/dist/db-postgres/schema/shop/order.d.ts +37 -1
  41. package/dist/db-postgres/schema/shop/order.js +3 -1
  42. package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
  43. package/dist/db-postgres/schema/shop/payment.js +4 -1
  44. package/dist/db-postgres/schema/shop/product.d.ts +20 -0
  45. package/dist/db-postgres/schema/shop/product.js +3 -1
  46. package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
  47. package/dist/db-postgres/schema/shop/productVariant.js +22 -0
  48. package/dist/paraglide/messages/_index.d.ts +36 -3
  49. package/dist/paraglide/messages/_index.js +71 -3
  50. package/dist/paraglide/messages/en.d.ts +5 -0
  51. package/dist/paraglide/messages/en.js +14 -0
  52. package/dist/paraglide/messages/pl.d.ts +5 -0
  53. package/dist/paraglide/messages/pl.js +14 -0
  54. package/dist/shop/cart/types.d.ts +1 -0
  55. package/dist/shop/client/index.d.ts +54 -0
  56. package/dist/shop/client/index.js +5 -1
  57. package/dist/shop/expiry.d.ts +35 -0
  58. package/dist/shop/expiry.js +68 -0
  59. package/dist/shop/http/balance-handler.d.ts +20 -0
  60. package/dist/shop/http/balance-handler.js +91 -0
  61. package/dist/shop/http/cart-handler.js +19 -0
  62. package/dist/shop/http/checkout-handler.js +19 -1
  63. package/dist/shop/http/index.d.ts +2 -0
  64. package/dist/shop/http/index.js +2 -0
  65. package/dist/shop/http/upcoming-handler.d.ts +16 -0
  66. package/dist/shop/http/upcoming-handler.js +65 -0
  67. package/dist/shop/http/webhook-handler.js +46 -9
  68. package/dist/shop/index.d.ts +4 -1
  69. package/dist/shop/index.js +7 -1
  70. package/dist/shop/server/balance-payment.d.ts +40 -0
  71. package/dist/shop/server/balance-payment.js +140 -0
  72. package/dist/shop/server/cart-hydrate.js +2 -0
  73. package/dist/shop/server/init.d.ts +14 -0
  74. package/dist/shop/server/init.js +35 -0
  75. package/dist/shop/server/orders.d.ts +34 -0
  76. package/dist/shop/server/orders.js +141 -2
  77. package/dist/shop/server/payment-policy.d.ts +35 -0
  78. package/dist/shop/server/payment-policy.js +55 -0
  79. package/dist/shop/server/payments.d.ts +29 -0
  80. package/dist/shop/server/payments.js +64 -0
  81. package/dist/shop/server/populate.d.ts +1 -1
  82. package/dist/shop/server/refund.d.ts +17 -12
  83. package/dist/shop/server/refund.js +96 -13
  84. package/dist/shop/server/shop-data.d.ts +4 -1
  85. package/dist/shop/server/shop-data.js +24 -2
  86. package/dist/shop/template.d.ts +13 -0
  87. package/dist/shop/template.js +98 -0
  88. package/dist/shop/types.d.ts +142 -1
  89. package/dist/shop/variant-attributes.d.ts +28 -0
  90. package/dist/shop/variant-attributes.js +69 -0
  91. package/dist/sveltekit/server/index.d.ts +1 -0
  92. package/dist/sveltekit/server/index.js +2 -0
  93. package/dist/types/cms.d.ts +4 -3
  94. package/dist/types/cms.schema.d.ts +1 -1
  95. package/dist/types/cms.schema.js +9 -0
  96. package/dist/types/fields.d.ts +21 -2
  97. package/dist/types/index.d.ts +1 -1
  98. package/dist/types/index.js +1 -1
  99. package/dist/types/plugins.d.ts +40 -0
  100. package/dist/types/plugins.js +4 -1
  101. package/dist/updates/0.26.1/index.d.ts +2 -0
  102. package/dist/updates/0.26.1/index.js +19 -0
  103. package/dist/updates/0.27.0/index.d.ts +2 -0
  104. package/dist/updates/0.27.0/index.js +50 -0
  105. package/dist/updates/index.js +5 -1
  106. package/package.json +1 -1
  107. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  108. package/dist/paraglide/messages/hello_world.js +0 -33
  109. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  110. package/dist/paraglide/messages/login_hello.js +0 -34
  111. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  112. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -8,6 +8,9 @@ export declare const getShopConfig: import("@sveltejs/kit").RemoteQueryFunction<
8
8
  id: string;
9
9
  label: import("../../shop/types.js").I18nText;
10
10
  }[];
11
+ variantAttributes: Record<string, import("../../shop/types.js").VariantAttribute>;
12
+ variantLabel: import("../../shop/types.js").VariantLabelConfig | null;
13
+ variantExpiry: import("../../shop/types.js").VariantExpiryConfig | null;
11
14
  } | null>;
12
15
  export declare const listShopProductEntries: import("@sveltejs/kit").RemoteQueryFunction<void, import("../../shop/server/shop-data.js").ShopEntryListItem[]>;
13
16
  export declare const getShopDataForEntry: import("@sveltejs/kit").RemoteQueryFunction<string, import("../../shop/server/shop-data.js").ShopDataWithVariants | null>;
@@ -18,6 +21,18 @@ export declare const upsertShopDataForEntry: import("@sveltejs/kit").RemoteComma
18
21
  vatRate: number;
19
22
  isActive?: boolean | undefined;
20
23
  sortOrder?: number | null | undefined;
24
+ paymentPolicy?: {
25
+ type: "full";
26
+ } | {
27
+ type: "deposit";
28
+ depositAmount: {
29
+ type: "percent";
30
+ value: number;
31
+ } | {
32
+ type: "amount";
33
+ value: number;
34
+ };
35
+ } | null | undefined;
21
36
  };
22
37
  variants?: {
23
38
  id?: string | undefined;
@@ -25,7 +40,7 @@ export declare const upsertShopDataForEntry: import("@sveltejs/kit").RemoteComma
25
40
  name?: Record<string, string> | null | undefined;
26
41
  priceDelta?: number | undefined;
27
42
  stock?: number | null | undefined;
28
- attributes?: Record<string, string> | null | undefined;
43
+ attributes?: Record<string, unknown> | null | undefined;
29
44
  }[] | undefined;
30
45
  }, Promise<import("../../shop/server/shop-data.js").ShopDataWithVariants>>;
31
46
  export declare const deleteShopDataForEntry: import("@sveltejs/kit").RemoteCommand<string, Promise<{
@@ -84,13 +99,18 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
84
99
  } | undefined, {
85
100
  items: {
86
101
  number: string;
102
+ currency: string;
103
+ consents: {
104
+ id: string;
105
+ accepted: boolean;
106
+ label: string;
107
+ }[] | null;
87
108
  id: string;
88
109
  status: import("../../shop/types.js").OrderStatus;
89
110
  createdAt: Date;
90
111
  updatedAt: Date;
91
112
  language: string | null;
92
113
  carrierType: string | null;
93
- currency: string;
94
114
  customerEmail: string;
95
115
  customerName: string | null;
96
116
  customerPhone: string | null;
@@ -108,13 +128,10 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
108
128
  shipmentCreatedAt: Date | null;
109
129
  paymentMethod: string | null;
110
130
  paymentProviderRef: string | null;
111
- consents: {
112
- id: string;
113
- accepted: boolean;
114
- label: string;
115
- }[] | null;
116
131
  notes: string | null;
117
132
  accessToken: string;
133
+ partialPayment: import("../../shop/types.js").PartialPayment | null;
134
+ balanceOwed: boolean;
118
135
  }[];
119
136
  total: number;
120
137
  limit: number;
@@ -123,13 +140,18 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
123
140
  export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFunction<string, {
124
141
  order: {
125
142
  number: string;
143
+ currency: string;
144
+ consents: {
145
+ id: string;
146
+ accepted: boolean;
147
+ label: string;
148
+ }[] | null;
126
149
  id: string;
127
150
  status: import("../../shop/types.js").OrderStatus;
128
151
  createdAt: Date;
129
152
  updatedAt: Date;
130
153
  language: string | null;
131
154
  carrierType: string | null;
132
- currency: string;
133
155
  customerEmail: string;
134
156
  customerName: string | null;
135
157
  customerPhone: string | null;
@@ -147,13 +169,10 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
147
169
  shipmentCreatedAt: Date | null;
148
170
  paymentMethod: string | null;
149
171
  paymentProviderRef: string | null;
150
- consents: {
151
- id: string;
152
- accepted: boolean;
153
- label: string;
154
- }[] | null;
155
172
  notes: string | null;
156
173
  accessToken: string;
174
+ partialPayment: import("../../shop/types.js").PartialPayment | null;
175
+ balanceOwed: boolean;
157
176
  };
158
177
  items: {
159
178
  id: string;
@@ -182,13 +201,18 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
182
201
  note?: string | undefined;
183
202
  }, Promise<{
184
203
  number: string;
204
+ currency: string;
205
+ consents: {
206
+ id: string;
207
+ accepted: boolean;
208
+ label: string;
209
+ }[] | null;
185
210
  id: string;
186
211
  status: import("../../shop/types.js").OrderStatus;
187
212
  createdAt: Date;
188
213
  updatedAt: Date;
189
214
  language: string | null;
190
215
  carrierType: string | null;
191
- currency: string;
192
216
  customerEmail: string;
193
217
  customerName: string | null;
194
218
  customerPhone: string | null;
@@ -206,13 +230,10 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
206
230
  shipmentCreatedAt: Date | null;
207
231
  paymentMethod: string | null;
208
232
  paymentProviderRef: string | null;
209
- consents: {
210
- id: string;
211
- accepted: boolean;
212
- label: string;
213
- }[] | null;
214
233
  notes: string | null;
215
234
  accessToken: string;
235
+ partialPayment: import("../../shop/types.js").PartialPayment | null;
236
+ balanceOwed: boolean;
216
237
  }>>;
217
238
  export declare const resendOrderEmailCmd: import("@sveltejs/kit").RemoteCommand<{
218
239
  orderId: string;
@@ -253,15 +274,15 @@ export declare const listShopableCollections: import("@sveltejs/kit").RemoteQuer
253
274
  }[]>;
254
275
  export declare const getOrderRefundsAdmin: import("@sveltejs/kit").RemoteQueryFunction<string, {
255
276
  refunds: {
277
+ amount: number;
278
+ currency: string;
256
279
  id: string;
257
280
  status: import("../../db-postgres/schema/shop/index.js").ShopRefundStatus;
258
281
  createdAt: Date;
259
282
  updatedAt: Date;
260
- currency: string;
261
283
  orderId: string;
262
284
  provider: string;
263
285
  providerRef: string | null;
264
- amount: number;
265
286
  paymentId: string | null;
266
287
  reason: string | null;
267
288
  createdBy: string | null;
@@ -277,6 +298,8 @@ export declare const refundOrderCmd: import("@sveltejs/kit").RemoteCommand<{
277
298
  orderId: string;
278
299
  amount?: number | undefined;
279
300
  reason?: string | undefined;
301
+ kind?: "full" | "deposit" | "balance" | undefined;
302
+ releaseStock?: boolean | undefined;
280
303
  }, Promise<{
281
304
  refund: import("../../shop/server/refund.js").ShopRefundRow;
282
305
  remainingRefundable: number;
@@ -286,9 +309,20 @@ export declare const refundOrderCmd: import("@sveltejs/kit").RemoteCommand<{
286
309
  error?: undefined;
287
310
  } | {
288
311
  success: false;
289
- code: "order_not_found" | "order_not_paid" | "no_provider_ref" | "unknown_provider" | "refund_unsupported" | "invalid_amount" | "amount_exceeds_remaining" | "provider_error";
312
+ code: "order_not_found" | "order_not_paid" | "no_provider_ref" | "unknown_provider" | "refund_unsupported" | "invalid_amount" | "amount_exceeds_remaining" | "provider_error" | "no_payment_kind";
290
313
  error: string;
291
314
  }>>;
315
+ export declare const generateBalanceLinkForOrder: import("@sveltejs/kit").RemoteCommand<string, Promise<{
316
+ success: false;
317
+ error: string;
318
+ url?: undefined;
319
+ balanceAmount?: undefined;
320
+ } | {
321
+ success: true;
322
+ url: string;
323
+ balanceAmount: number;
324
+ error?: undefined;
325
+ }>>;
292
326
  export declare const listCouponsAdmin: import("@sveltejs/kit").RemoteQueryFunction<void, {
293
327
  id: string;
294
328
  code: string;
@@ -317,7 +351,7 @@ export declare const getCouponAdmin: import("@sveltejs/kit").RemoteQueryFunction
317
351
  }>;
318
352
  export declare const createCouponCmd: import("@sveltejs/kit").RemoteCommand<{
319
353
  code: string;
320
- type: "fixed" | "percent";
354
+ type: "percent" | "fixed";
321
355
  value: number;
322
356
  minOrderAmount?: number | null | undefined;
323
357
  maxUses?: number | null | undefined;
@@ -340,7 +374,7 @@ export declare const updateCouponCmd: import("@sveltejs/kit").RemoteCommand<{
340
374
  id: string;
341
375
  input: {
342
376
  code?: string | undefined;
343
- type?: "fixed" | "percent" | undefined;
377
+ type?: "percent" | "fixed" | undefined;
344
378
  value?: number | undefined;
345
379
  minOrderAmount?: number | null | undefined;
346
380
  maxUses?: number | null | undefined;
@@ -24,7 +24,10 @@ export const getShopConfig = query(async () => {
24
24
  vatRates: shop.vatRates,
25
25
  features: shop.features,
26
26
  languages: getCMS().languages,
27
- paymentMethods: shop.payment.map((p) => ({ id: p.id, label: p.label }))
27
+ paymentMethods: shop.payment.map((p) => ({ id: p.id, label: p.label })),
28
+ variantAttributes: shop.variantAttributes,
29
+ variantLabel: shop.variantLabel,
30
+ variantExpiry: shop.variantExpiry
28
31
  };
29
32
  });
30
33
  export const listShopProductEntries = query(async () => {
@@ -35,11 +38,20 @@ export const getShopDataForEntry = query(z.string(), async (entryId) => {
35
38
  requireAuth();
36
39
  return getShopDataByEntry(entryId);
37
40
  });
41
+ const depositAmountSchema = z.discriminatedUnion('type', [
42
+ z.object({ type: z.literal('percent'), value: z.number().positive().max(100) }),
43
+ z.object({ type: z.literal('amount'), value: z.number().int().positive() })
44
+ ]);
45
+ const paymentPolicySchema = z.discriminatedUnion('type', [
46
+ z.object({ type: z.literal('full') }),
47
+ z.object({ type: z.literal('deposit'), depositAmount: depositAmountSchema })
48
+ ]);
38
49
  const shopDataInputSchema = z.object({
39
50
  basePrice: z.number().nonnegative().max(1e9), // PLN (≤6dp)
40
51
  vatRate: z.number().int().min(0).max(100),
41
52
  isActive: z.boolean().optional(),
42
- sortOrder: z.number().int().nullable().optional()
53
+ sortOrder: z.number().int().nullable().optional(),
54
+ paymentPolicy: paymentPolicySchema.nullable().optional()
43
55
  });
44
56
  const variantInputSchema = z.object({
45
57
  id: z.string().optional(),
@@ -47,7 +59,9 @@ const variantInputSchema = z.object({
47
59
  name: z.record(z.string(), z.string()).nullable().optional(),
48
60
  priceDelta: z.number().optional(), // PLN
49
61
  stock: z.number().int().nullable().optional(),
50
- attributes: z.record(z.string(), z.string()).nullable().optional()
62
+ // unknown — variantAttributes can be string|number|boolean|datetime;
63
+ // validateVariantAttributes (server) enforces the typed shape from shop config.
64
+ attributes: z.record(z.string(), z.unknown()).nullable().optional()
51
65
  });
52
66
  export const upsertShopDataForEntry = command(z.object({
53
67
  entryId: z.string(),
@@ -223,11 +237,20 @@ export const getOrderRefundsAdmin = query(z.string(), async (orderId) => {
223
237
  export const refundOrderCmd = command(z.object({
224
238
  orderId: z.string(),
225
239
  amount: z.number().int().positive().optional(),
226
- reason: z.string().max(500).optional()
227
- }), async ({ orderId, amount, reason }) => {
240
+ reason: z.string().max(500).optional(),
241
+ kind: z.enum(['full', 'deposit', 'balance']).optional(),
242
+ releaseStock: z.boolean().optional()
243
+ }), async ({ orderId, amount, reason, kind, releaseStock }) => {
228
244
  requireAuth();
229
245
  try {
230
- const result = await refundOrder({ orderId, amount, reason, createdBy: 'admin' });
246
+ const result = await refundOrder({
247
+ orderId,
248
+ amount,
249
+ reason,
250
+ kind,
251
+ releaseStock,
252
+ createdBy: 'admin'
253
+ });
231
254
  return { success: true, ...result };
232
255
  }
233
256
  catch (err) {
@@ -238,6 +261,38 @@ export const refundOrderCmd = command(z.object({
238
261
  return { success: false, code: 'provider_error', error: message };
239
262
  }
240
263
  });
264
+ export const generateBalanceLinkForOrder = command(z.string(), async (orderId) => {
265
+ requireAuth();
266
+ const { generateBalanceToken, requireBalanceTokenSecret } = await import('../../shop/server/balance-payment.js');
267
+ const { getOrderById } = await import('../../shop/server/orders.js');
268
+ const order = await getOrderById(orderId);
269
+ if (!order)
270
+ return { success: false, error: 'Order not found' };
271
+ if (!order.balanceOwed) {
272
+ return { success: false, error: 'Order has no outstanding balance' };
273
+ }
274
+ let secret;
275
+ try {
276
+ secret = requireBalanceTokenSecret();
277
+ }
278
+ catch (err) {
279
+ const message = err instanceof Error ? err.message : 'Token secret not configured';
280
+ return { success: false, error: message };
281
+ }
282
+ const token = generateBalanceToken(order.id, secret);
283
+ // Mirror orderViewUrl template — build the customer-facing URL by
284
+ // extending the configured base with the balance path + token.
285
+ const cms = (await import('../../core/cms.js')).getCMS();
286
+ const orderViewUrl = cms.shopConfig?.orderViewUrl ?? '/shop/order/{orderNumber}?token={accessToken}';
287
+ const base = orderViewUrl
288
+ .replace('{orderNumber}', encodeURIComponent(order.number))
289
+ .replace('{orderId}', encodeURIComponent(order.id))
290
+ .replace('{accessToken}', encodeURIComponent(order.accessToken))
291
+ .replace('{language}', encodeURIComponent(order.language ?? ''));
292
+ const sep = base.includes('?') ? '&' : '?';
293
+ const url = `${base}${sep}balance=1&balanceToken=${encodeURIComponent(token)}`;
294
+ return { success: true, url, balanceAmount: order.partialPayment?.balanceAmount ?? 0 };
295
+ });
241
296
  // Coupons ────────────────────────────────────────────────────────────────────
242
297
  const couponInputSchema = z.object({
243
298
  code: z.string().min(1).max(64),
@@ -0,0 +1,9 @@
1
+ import type { IconSetPlugin } from '../../types/plugins.js';
2
+ export declare function setIconSets(sets: Map<string, IconSetPlugin>): void;
3
+ export declare function getIconSets(): Map<string, IconSetPlugin>;
4
+ /**
5
+ * Resolve a single icon set: explicit `slug` (from {@link IconField.set}) takes
6
+ * precedence; otherwise returns the first registered set (predictable order
7
+ * matches Map insertion). Returns `null` when none are registered.
8
+ */
9
+ export declare function resolveIconSet(slug?: string): IconSetPlugin | null;
@@ -0,0 +1,20 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const contextKey = Symbol('iconSets');
3
+ export function setIconSets(sets) {
4
+ setContext(contextKey, sets);
5
+ }
6
+ export function getIconSets() {
7
+ return getContext(contextKey) ?? new Map();
8
+ }
9
+ /**
10
+ * Resolve a single icon set: explicit `slug` (from {@link IconField.set}) takes
11
+ * precedence; otherwise returns the first registered set (predictable order
12
+ * matches Map insertion). Returns `null` when none are registered.
13
+ */
14
+ export function resolveIconSet(slug) {
15
+ const sets = getIconSets();
16
+ if (slug)
17
+ return sets.get(slug) ?? null;
18
+ const first = sets.values().next();
19
+ return first.done ? null : first.value;
20
+ }
@@ -329,7 +329,7 @@ export const { POST } = createRetryPaymentHandler();
329
329
  {
330
330
  path: 'admin/api/[...path]/+server.ts',
331
331
  content: `${GENERATED_COMMENT_TS}
332
- import { createAdminApiHandler } from 'includio-cms/admin/api/handler';
332
+ import { createAdminApiHandler } from 'includio-cms/sveltekit/server';
333
333
 
334
334
  export const { GET, POST, PATCH, PUT, DELETE } = createAdminApiHandler();
335
335
  `
@@ -337,7 +337,7 @@ export const { GET, POST, PATCH, PUT, DELETE } = createAdminApiHandler();
337
337
  {
338
338
  path: 'admin/api/rest/[...restPath]/+server.ts',
339
339
  content: `${GENERATED_COMMENT_TS}
340
- import { createRestApiHandler } from 'includio-cms/admin/api/rest/handler';
340
+ import { createRestApiHandler } from 'includio-cms/sveltekit/server';
341
341
 
342
342
  export const { GET, POST, PUT, DELETE } = createRestApiHandler();
343
343
  `
@@ -17,7 +17,7 @@
17
17
  bind:ref
18
18
  data-slot="checkbox"
19
19
  class={cn(
20
- "border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-6 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
20
+ "border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] after:absolute after:-inset-1 after:content-[''] disabled:cursor-not-allowed disabled:opacity-50",
21
21
  className
22
22
  )}
23
23
  bind:checked
@@ -27,9 +27,9 @@
27
27
  {#snippet children({ checked, indeterminate })}
28
28
  <div data-slot="checkbox-indicator" class="text-current transition-none">
29
29
  {#if checked}
30
- <CheckIcon class="size-4" />
30
+ <CheckIcon class="size-3.5" />
31
31
  {:else if indeterminate}
32
- <MinusIcon class="size-4" />
32
+ <MinusIcon class="size-3.5" />
33
33
  {/if}
34
34
  </div>
35
35
  {/snippet}
@@ -4,7 +4,7 @@ import type { ApiKeyConfig, AuthConfig, CMSConfig, ICMS, MediaConfig, Typography
4
4
  import type { CollectionConfigWithType } from '../types/collections.js';
5
5
  import type { Language } from '../types/languages.js';
6
6
  import type { SingleConfigWithType } from '../types/singles.js';
7
- import type { CustomFieldDefinition, PluginConfig } from '../types/plugins.js';
7
+ import type { CustomFieldDefinition, IconSetPlugin, Plugin } from '../types/plugins.js';
8
8
  import type { FormConfig } from '../types/forms.js';
9
9
  import type { AIAdapter } from '../types/adapters/ai.js';
10
10
  import type { EmailAdapter } from '../types/adapters/email.js';
@@ -28,8 +28,17 @@ export declare class CMS implements ICMS {
28
28
  sidebarHelp: boolean;
29
29
  shopConfig: ResolvedShopConfig | null;
30
30
  cmpConfig: ResolvedCmpConfig | null;
31
- plugins: PluginConfig[];
31
+ /**
32
+ * Resolves once the shop's variant-attribute GIN indexes have been applied
33
+ * by `initCMS()`. `null` when the CMS is configured without a shop. Tests
34
+ * can `await` this to gate on init completion; production callers don't
35
+ * need to — index application is idempotent and non-blocking.
36
+ * @internal
37
+ */
38
+ shopInitPromise: Promise<void> | null;
39
+ plugins: Plugin[];
32
40
  customFields: Map<string, CustomFieldDefinition>;
41
+ iconSets: Map<string, IconSetPlugin>;
33
42
  apiKeys: ApiKeyConfig[];
34
43
  constructor(config: CMSConfig);
35
44
  private validateFieldSlugs;
package/dist/core/cms.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { isIconSetPlugin } from '../types/plugins.js';
1
2
  import { setSchemaGetCMS } from './fields/fieldSchemaToTs.js';
2
3
  import { betterAuth } from 'better-auth';
3
4
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
4
5
  import { admin } from 'better-auth/plugins';
5
6
  import { resetPasswordEmailTemplate } from '../admin/email/reset-password-template.js';
6
7
  import * as authSchema from '../server/db/schema/auth-schema.js';
8
+ import { applyVariantAttributeIndexes } from '../shop/server/init.js';
7
9
  export class CMS {
8
10
  config;
9
11
  databaseAdapter;
@@ -21,8 +23,17 @@ export class CMS {
21
23
  sidebarHelp;
22
24
  shopConfig;
23
25
  cmpConfig;
26
+ /**
27
+ * Resolves once the shop's variant-attribute GIN indexes have been applied
28
+ * by `initCMS()`. `null` when the CMS is configured without a shop. Tests
29
+ * can `await` this to gate on init completion; production callers don't
30
+ * need to — index application is idempotent and non-blocking.
31
+ * @internal
32
+ */
33
+ shopInitPromise = null;
24
34
  plugins = [];
25
35
  customFields = new Map();
36
+ iconSets = new Map();
26
37
  apiKeys = [];
27
38
  constructor(config) {
28
39
  this.config = config;
@@ -64,6 +75,13 @@ export class CMS {
64
75
  if (config.plugins) {
65
76
  this.plugins = config.plugins;
66
77
  for (const plugin of this.plugins) {
78
+ if (isIconSetPlugin(plugin)) {
79
+ if (this.iconSets.has(plugin.slug)) {
80
+ throw new Error(`Duplicate icon-set plugin slug: "${plugin.slug}"`);
81
+ }
82
+ this.iconSets.set(plugin.slug, plugin);
83
+ continue;
84
+ }
67
85
  for (const def of plugin.fields ?? []) {
68
86
  if (this.customFields.has(def.fieldType)) {
69
87
  throw new Error(`Duplicate custom field type: "${def.fieldType}" (plugin: "${plugin.slug}")`);
@@ -157,6 +175,17 @@ export function initCMS(config) {
157
175
  import('./server/media/operations/backgroundMaintenance.js')
158
176
  .then((m) => m.startBackgroundMaintenance())
159
177
  .catch((e) => console.warn('[cms] Failed to start background maintenance:', e));
178
+ // Apply shop variantAttribute GIN indexes (idempotent CREATE INDEX IF NOT EXISTS).
179
+ // Pass shop + drizzle explicitly so the dynamic import doesn't depend on a
180
+ // shared CMS singleton (vitest can give the dynamic module a fresh instance).
181
+ if (cms.shopConfig) {
182
+ const drizzle = cms.databaseAdapter._drizzle;
183
+ if (drizzle) {
184
+ cms.shopInitPromise = applyVariantAttributeIndexes(cms.shopConfig, drizzle).catch((e) => {
185
+ console.warn('[shop] Failed to apply variant attribute indexes:', e);
186
+ });
187
+ }
188
+ }
160
189
  return cms;
161
190
  }
162
191
  /**
@@ -444,6 +444,13 @@ export function generateZodSchemaFromField(field, languages, options = {
444
444
  localizedDefault.text = emptyLangMap;
445
445
  return localizedSchema.optional().default(localizedDefault);
446
446
  }
447
+ case 'icon': {
448
+ // Value is a plain string key referencing an icon registered by an
449
+ // IconSetPlugin. Validation matches `slug` field semantics.
450
+ if (field.required)
451
+ return z.string().min(1, { message: msg.required });
452
+ return z.string().optional().default('');
453
+ }
447
454
  case 'custom': {
448
455
  const customDef = getCustomFieldDef(field.fieldType);
449
456
  if (!customDef)
@@ -1,6 +1,8 @@
1
1
  import type { Field } from '../../../types/fields.js';
2
2
  import type { CustomFieldDefinition } from '../../../types/plugins.js';
3
+ import type { VariantAttribute } from '../../../shop/types.js';
3
4
  export declare function setGeneratorCustomFields(customFields: Map<string, CustomFieldDefinition>): void;
5
+ export declare function setGeneratorShopVariantAttributes(attrs: Record<string, VariantAttribute>): void;
4
6
  export declare function generateTsTypeFromFields(fields: Field[]): string;
5
7
  export declare function generateFlatTsTypeFromFields(fields: Field[]): string;
6
8
  export interface InlineBlockTypeDef {
@@ -1,12 +1,44 @@
1
1
  import { toPascalCase, quoteKey } from './utils.js';
2
2
  import { buildSeoTsType } from '../../fields/seoFieldDescriptor.js';
3
3
  let _customFields = new Map();
4
+ let _shopVariantAttributes = {};
4
5
  function isGuaranteed(f) {
5
6
  return !!(f.required || f.type === 'seo' || f.defaultValue !== undefined);
6
7
  }
7
8
  export function setGeneratorCustomFields(customFields) {
8
9
  _customFields = customFields;
9
10
  }
11
+ export function setGeneratorShopVariantAttributes(attrs) {
12
+ _shopVariantAttributes = attrs;
13
+ }
14
+ function variantAttributeTsType(attr) {
15
+ switch (attr.type) {
16
+ case 'text':
17
+ case 'datetime':
18
+ case 'image':
19
+ case 'entry':
20
+ case 'slug':
21
+ return 'string';
22
+ case 'number':
23
+ return 'number';
24
+ case 'boolean':
25
+ return 'boolean';
26
+ case 'select':
27
+ return attr.options.map((o) => `'${o.value}'`).join(' | ');
28
+ }
29
+ }
30
+ function buildShopAttributesType() {
31
+ const entries = Object.entries(_shopVariantAttributes);
32
+ if (entries.length === 0)
33
+ return 'Record<string, string> | null';
34
+ const fields = entries
35
+ .map(([key, attr]) => {
36
+ const opt = attr.required ? '' : '?';
37
+ return `${quoteKey(key)}${opt}: ${variantAttributeTsType(attr)}`;
38
+ })
39
+ .join('; ');
40
+ return `{ ${fields} }`;
41
+ }
10
42
  function getFieldTypeAsString(field) {
11
43
  switch (field.type) {
12
44
  case 'text':
@@ -68,6 +100,7 @@ function getFieldTypeAsString(field) {
68
100
  return 'UrlFieldData[]';
69
101
  }
70
102
  case 'slug':
103
+ case 'icon':
71
104
  return 'string';
72
105
  case 'seo':
73
106
  return buildSeoTsType();
@@ -82,7 +115,7 @@ function getFieldTypeAsString(field) {
82
115
  name: Record<string, string> | null;
83
116
  priceDelta: number;
84
117
  stock: number | null;
85
- attributes: Record<string, string> | null;
118
+ attributes: ${buildShopAttributesType()};
86
119
  }>;
87
120
  } | null`;
88
121
  case 'url': {
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { generateTsTypeFromFields, generateFlatTsTypeFromFields, generateInlineBlockTypeString, setGeneratorCustomFields } from './fields.js';
3
+ import { generateTsTypeFromFields, generateFlatTsTypeFromFields, generateInlineBlockTypeString, setGeneratorCustomFields, setGeneratorShopVariantAttributes } from './fields.js';
4
4
  import { generateTsTypeFromFormFields } from './formFields.js';
5
5
  import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
6
6
  import { toPascalCase, quoteKey } from './utils.js';
@@ -312,6 +312,7 @@ export function generateRuntime(config) {
312
312
  }
313
313
  }
314
314
  setGeneratorCustomFields(customFields);
315
+ setGeneratorShopVariantAttributes(config.shop?.variantAttributes ?? {});
315
316
  createCmsRuntimeDir();
316
317
  generateTypes(config);
317
318
  generateAPI(config);
@@ -1,4 +1,4 @@
1
- import type { OrderStatus } from '../../../shop/types.js';
1
+ import type { OrderStatus, PartialPayment } from '../../../shop/types.js';
2
2
  export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
3
3
  name: "shop_orders";
4
4
  schema: undefined;
@@ -459,6 +459,42 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
459
459
  identity: undefined;
460
460
  generated: undefined;
461
461
  }, {}, {}>;
462
+ partialPayment: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
463
+ name: "partial_payment";
464
+ tableName: "shop_orders";
465
+ dataType: "json";
466
+ columnType: "PgJsonb";
467
+ data: PartialPayment | null;
468
+ driverParam: unknown;
469
+ notNull: false;
470
+ hasDefault: false;
471
+ isPrimaryKey: false;
472
+ isAutoincrement: false;
473
+ hasRuntimeDefault: false;
474
+ enumValues: undefined;
475
+ baseColumn: never;
476
+ identity: undefined;
477
+ generated: undefined;
478
+ }, {}, {
479
+ $type: PartialPayment | null;
480
+ }>;
481
+ balanceOwed: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
482
+ name: "balance_owed";
483
+ tableName: "shop_orders";
484
+ dataType: "boolean";
485
+ columnType: "PgBoolean";
486
+ data: boolean;
487
+ driverParam: boolean;
488
+ notNull: true;
489
+ hasDefault: true;
490
+ isPrimaryKey: false;
491
+ isAutoincrement: false;
492
+ hasRuntimeDefault: false;
493
+ enumValues: undefined;
494
+ baseColumn: never;
495
+ identity: undefined;
496
+ generated: undefined;
497
+ }, {}, {}>;
462
498
  createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
463
499
  name: "created_at";
464
500
  tableName: "shop_orders";
@@ -1,4 +1,4 @@
1
- import { integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
1
+ import { boolean, integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
2
  import { shopShippingMethodsTable } from './shippingMethod.js';
3
3
  export const shopOrdersTable = pgTable('shop_orders', {
4
4
  id: uuid('id').primaryKey().defaultRandom(),
@@ -29,6 +29,8 @@ export const shopOrdersTable = pgTable('shop_orders', {
29
29
  notes: text('notes'),
30
30
  language: text('language'),
31
31
  accessToken: uuid('access_token').defaultRandom().notNull(),
32
+ partialPayment: jsonb('partial_payment').$type(),
33
+ balanceOwed: boolean('balance_owed').default(false).notNull(),
32
34
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
33
35
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
34
36
  });