@empty-complete-org/medusa-product-attributes 0.11.1 → 0.12.1

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.
@@ -14,7 +14,7 @@ const sdk = new Medusa__default.default({
14
14
  type: "session"
15
15
  }
16
16
  });
17
- const emptyForm = () => ({ label: "", type: "text" });
17
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
18
18
  const CategoryAttributeTemplatesWidget = ({
19
19
  data
20
20
  }) => {
@@ -65,10 +65,11 @@ const CategoryAttributeTemplatesWidget = ({
65
65
  if (!addForm.label.trim()) return;
66
66
  createMutation.mutate({
67
67
  label: addForm.label.trim(),
68
- type: addForm.type
68
+ type: addForm.type,
69
+ unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
69
70
  });
70
71
  };
71
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t;
72
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
72
73
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
73
74
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
74
75
  /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
@@ -92,7 +93,10 @@ const CategoryAttributeTemplatesWidget = ({
92
93
  className: "flex items-center gap-3 px-6 py-3",
93
94
  children: [
94
95
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
95
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
96
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
97
+ typeLabel(attr.type),
98
+ attr.unit ? `, ${attr.unit}` : ""
99
+ ] }),
96
100
  /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
97
101
  ]
98
102
  },
@@ -140,7 +144,10 @@ const CategoryAttributeTemplatesWidget = ({
140
144
  className: "flex items-center gap-3 px-6 py-3",
141
145
  children: [
142
146
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
143
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
147
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
148
+ typeLabel(attr.type),
149
+ attr.unit ? `, ${attr.unit}` : ""
150
+ ] }),
144
151
  /* @__PURE__ */ jsxRuntime.jsx(
145
152
  "button",
146
153
  {
@@ -173,15 +180,27 @@ const CategoryAttributeTemplatesWidget = ({
173
180
  value: addForm.type,
174
181
  onChange: (e) => setAddForm((f) => ({
175
182
  ...f,
176
- type: e.target.value
183
+ type: e.target.value,
184
+ unit: e.target.value === "number" ? f.unit : ""
177
185
  })),
178
186
  className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
179
187
  children: [
180
188
  /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
181
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" })
189
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
190
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
191
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
182
192
  ]
183
193
  }
184
194
  ),
195
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
196
+ ui.Input,
197
+ {
198
+ value: addForm.unit,
199
+ onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
200
+ placeholder: "ед. (кг, м, шт...)",
201
+ className: "w-28 h-8 text-sm"
202
+ }
203
+ ),
185
204
  /* @__PURE__ */ jsxRuntime.jsx(
186
205
  ui.Button,
187
206
  {
@@ -220,6 +239,8 @@ const ProductAttributeValuesWidget = ({
220
239
  const qc = reactQuery.useQueryClient();
221
240
  const [formValues, setFormValues] = react.useState({});
222
241
  const [saveSuccess, setSaveSuccess] = react.useState(false);
242
+ const [uploadingId, setUploadingId] = react.useState(null);
243
+ const fileInputs = react.useRef({});
223
244
  const attributesQuery = reactQuery.useQuery({
224
245
  queryKey: ["category-custom-attributes", categoryId],
225
246
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
@@ -235,7 +256,12 @@ const ProductAttributeValuesWidget = ({
235
256
  const values = valuesQuery.data.product_custom_attributes;
236
257
  const initial = {};
237
258
  for (const attr of attributes2) {
238
- const existing = values.find((v) => v.id === attr.id);
259
+ const existing = values.find(
260
+ (v) => {
261
+ var _a2;
262
+ return ((_a2 = v.category_custom_attribute) == null ? void 0 : _a2.id) === attr.id || v.category_custom_attribute_id === attr.id;
263
+ }
264
+ );
239
265
  initial[attr.id] = existing ? existing.value : "";
240
266
  }
241
267
  setFormValues(initial);
@@ -256,11 +282,27 @@ const ProductAttributeValuesWidget = ({
256
282
  const handleSave = () => {
257
283
  if (!attributesQuery.data) return;
258
284
  const attributes2 = attributesQuery.data.category_custom_attributes;
259
- const attributesToUpdate = attributes2.filter(
260
- (attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
261
- ).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
285
+ const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
262
286
  saveMutation.mutate({ attributes: attributesToUpdate });
263
287
  };
288
+ const handleFileUpload = async (attrId, file) => {
289
+ var _a2, _b2;
290
+ setUploadingId(attrId);
291
+ try {
292
+ const formData = new FormData();
293
+ formData.append("files", file);
294
+ const res = await sdk.client.fetch(`/admin/uploads`, {
295
+ method: "POST",
296
+ body: formData
297
+ });
298
+ const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
299
+ if (url) {
300
+ setFormValues((prev) => ({ ...prev, [attrId]: url }));
301
+ }
302
+ } finally {
303
+ setUploadingId(null);
304
+ }
305
+ };
264
306
  if (!categoryId) {
265
307
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
266
308
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
@@ -281,8 +323,68 @@ const ProductAttributeValuesWidget = ({
281
323
  {
282
324
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
283
325
  children: [
284
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
285
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(
326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
327
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
328
+ attr.unit && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: attr.unit })
329
+ ] }),
330
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxRuntime.jsxs(
331
+ "select",
332
+ {
333
+ value: formValues[attr.id] ?? "",
334
+ onChange: (e) => setFormValues((prev) => ({
335
+ ...prev,
336
+ [attr.id]: e.target.value
337
+ })),
338
+ className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
339
+ children: [
340
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "—" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "Да" }),
342
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "Нет" })
343
+ ]
344
+ }
345
+ ) : attr.type === "file" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
346
+ /* @__PURE__ */ jsxRuntime.jsx(
347
+ "input",
348
+ {
349
+ ref: (el) => {
350
+ fileInputs.current[attr.id] = el;
351
+ },
352
+ type: "file",
353
+ className: "hidden",
354
+ onChange: (e) => {
355
+ var _a2;
356
+ const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
357
+ if (f) handleFileUpload(attr.id, f);
358
+ }
359
+ }
360
+ ),
361
+ /* @__PURE__ */ jsxRuntime.jsx(
362
+ ui.Input,
363
+ {
364
+ value: formValues[attr.id] ?? "",
365
+ onChange: (e) => setFormValues((prev) => ({
366
+ ...prev,
367
+ [attr.id]: e.target.value
368
+ })),
369
+ placeholder: "URL файла",
370
+ className: "h-8 text-sm flex-1"
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsxRuntime.jsx(
374
+ ui.Button,
375
+ {
376
+ size: "small",
377
+ variant: "secondary",
378
+ type: "button",
379
+ onClick: () => {
380
+ var _a2;
381
+ return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
382
+ },
383
+ isLoading: uploadingId === attr.id,
384
+ children: "Загрузить"
385
+ }
386
+ )
387
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
286
388
  ui.Input,
287
389
  {
288
390
  type: attr.type === "number" ? "number" : "text",
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { defineWidgetConfig } from "@medusajs/admin-sdk";
3
3
  import { Container, Heading, Button, Text, Badge, Input } from "@medusajs/ui";
4
4
  import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
5
- import { useState, useEffect } from "react";
5
+ import { useState, useRef, useEffect } from "react";
6
6
  import Medusa from "@medusajs/js-sdk";
7
7
  const sdk = new Medusa({
8
8
  baseUrl: "/",
@@ -11,7 +11,7 @@ const sdk = new Medusa({
11
11
  type: "session"
12
12
  }
13
13
  });
14
- const emptyForm = () => ({ label: "", type: "text" });
14
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
15
15
  const CategoryAttributeTemplatesWidget = ({
16
16
  data
17
17
  }) => {
@@ -62,10 +62,11 @@ const CategoryAttributeTemplatesWidget = ({
62
62
  if (!addForm.label.trim()) return;
63
63
  createMutation.mutate({
64
64
  label: addForm.label.trim(),
65
- type: addForm.type
65
+ type: addForm.type,
66
+ unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
66
67
  });
67
68
  };
68
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t;
69
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
69
70
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
70
71
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
71
72
  /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
@@ -89,7 +90,10 @@ const CategoryAttributeTemplatesWidget = ({
89
90
  className: "flex items-center gap-3 px-6 py-3",
90
91
  children: [
91
92
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
92
- /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
93
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
94
+ typeLabel(attr.type),
95
+ attr.unit ? `, ${attr.unit}` : ""
96
+ ] }),
93
97
  /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
94
98
  ]
95
99
  },
@@ -137,7 +141,10 @@ const CategoryAttributeTemplatesWidget = ({
137
141
  className: "flex items-center gap-3 px-6 py-3",
138
142
  children: [
139
143
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
140
- /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
144
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
145
+ typeLabel(attr.type),
146
+ attr.unit ? `, ${attr.unit}` : ""
147
+ ] }),
141
148
  /* @__PURE__ */ jsx(
142
149
  "button",
143
150
  {
@@ -170,15 +177,27 @@ const CategoryAttributeTemplatesWidget = ({
170
177
  value: addForm.type,
171
178
  onChange: (e) => setAddForm((f) => ({
172
179
  ...f,
173
- type: e.target.value
180
+ type: e.target.value,
181
+ unit: e.target.value === "number" ? f.unit : ""
174
182
  })),
175
183
  className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
176
184
  children: [
177
185
  /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
178
- /* @__PURE__ */ jsx("option", { value: "number", children: "Число" })
186
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
187
+ /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
188
+ /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
179
189
  ]
180
190
  }
181
191
  ),
192
+ addForm.type === "number" && /* @__PURE__ */ jsx(
193
+ Input,
194
+ {
195
+ value: addForm.unit,
196
+ onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
197
+ placeholder: "ед. (кг, м, шт...)",
198
+ className: "w-28 h-8 text-sm"
199
+ }
200
+ ),
182
201
  /* @__PURE__ */ jsx(
183
202
  Button,
184
203
  {
@@ -217,6 +236,8 @@ const ProductAttributeValuesWidget = ({
217
236
  const qc = useQueryClient();
218
237
  const [formValues, setFormValues] = useState({});
219
238
  const [saveSuccess, setSaveSuccess] = useState(false);
239
+ const [uploadingId, setUploadingId] = useState(null);
240
+ const fileInputs = useRef({});
220
241
  const attributesQuery = useQuery({
221
242
  queryKey: ["category-custom-attributes", categoryId],
222
243
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
@@ -232,7 +253,12 @@ const ProductAttributeValuesWidget = ({
232
253
  const values = valuesQuery.data.product_custom_attributes;
233
254
  const initial = {};
234
255
  for (const attr of attributes2) {
235
- const existing = values.find((v) => v.id === attr.id);
256
+ const existing = values.find(
257
+ (v) => {
258
+ var _a2;
259
+ return ((_a2 = v.category_custom_attribute) == null ? void 0 : _a2.id) === attr.id || v.category_custom_attribute_id === attr.id;
260
+ }
261
+ );
236
262
  initial[attr.id] = existing ? existing.value : "";
237
263
  }
238
264
  setFormValues(initial);
@@ -253,11 +279,27 @@ const ProductAttributeValuesWidget = ({
253
279
  const handleSave = () => {
254
280
  if (!attributesQuery.data) return;
255
281
  const attributes2 = attributesQuery.data.category_custom_attributes;
256
- const attributesToUpdate = attributes2.filter(
257
- (attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
258
- ).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
282
+ const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
259
283
  saveMutation.mutate({ attributes: attributesToUpdate });
260
284
  };
285
+ const handleFileUpload = async (attrId, file) => {
286
+ var _a2, _b2;
287
+ setUploadingId(attrId);
288
+ try {
289
+ const formData = new FormData();
290
+ formData.append("files", file);
291
+ const res = await sdk.client.fetch(`/admin/uploads`, {
292
+ method: "POST",
293
+ body: formData
294
+ });
295
+ const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
296
+ if (url) {
297
+ setFormValues((prev) => ({ ...prev, [attrId]: url }));
298
+ }
299
+ } finally {
300
+ setUploadingId(null);
301
+ }
302
+ };
261
303
  if (!categoryId) {
262
304
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
263
305
  /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
@@ -278,8 +320,68 @@ const ProductAttributeValuesWidget = ({
278
320
  {
279
321
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
280
322
  children: [
281
- /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
282
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
323
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
324
+ /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
325
+ attr.unit && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: attr.unit })
326
+ ] }),
327
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxs(
328
+ "select",
329
+ {
330
+ value: formValues[attr.id] ?? "",
331
+ onChange: (e) => setFormValues((prev) => ({
332
+ ...prev,
333
+ [attr.id]: e.target.value
334
+ })),
335
+ className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
336
+ children: [
337
+ /* @__PURE__ */ jsx("option", { value: "", children: "—" }),
338
+ /* @__PURE__ */ jsx("option", { value: "true", children: "Да" }),
339
+ /* @__PURE__ */ jsx("option", { value: "false", children: "Нет" })
340
+ ]
341
+ }
342
+ ) : attr.type === "file" ? /* @__PURE__ */ jsxs(Fragment, { children: [
343
+ /* @__PURE__ */ jsx(
344
+ "input",
345
+ {
346
+ ref: (el) => {
347
+ fileInputs.current[attr.id] = el;
348
+ },
349
+ type: "file",
350
+ className: "hidden",
351
+ onChange: (e) => {
352
+ var _a2;
353
+ const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
354
+ if (f) handleFileUpload(attr.id, f);
355
+ }
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsx(
359
+ Input,
360
+ {
361
+ value: formValues[attr.id] ?? "",
362
+ onChange: (e) => setFormValues((prev) => ({
363
+ ...prev,
364
+ [attr.id]: e.target.value
365
+ })),
366
+ placeholder: "URL файла",
367
+ className: "h-8 text-sm flex-1"
368
+ }
369
+ ),
370
+ /* @__PURE__ */ jsx(
371
+ Button,
372
+ {
373
+ size: "small",
374
+ variant: "secondary",
375
+ type: "button",
376
+ onClick: () => {
377
+ var _a2;
378
+ return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
379
+ },
380
+ isLoading: uploadingId === attr.id,
381
+ children: "Загрузить"
382
+ }
383
+ )
384
+ ] }) : /* @__PURE__ */ jsx(
283
385
  Input,
284
386
  {
285
387
  type: attr.type === "number" ? "number" : "text",
@@ -0,0 +1,5 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ export declare class Migration20260405120000 extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Migration20260405120000 = void 0;
4
+ const migrations_1 = require("@mikro-orm/migrations");
5
+ class Migration20260405120000 extends migrations_1.Migration {
6
+ async up() {
7
+ this.addSql(`
8
+ CREATE TABLE IF NOT EXISTS "category_custom_attribute" (
9
+ "id" text NOT NULL,
10
+ "key" text NOT NULL,
11
+ "type" text NOT NULL DEFAULT 'text',
12
+ "label" text NOT NULL DEFAULT '',
13
+ "unit" text NULL,
14
+ "sort_order" integer NOT NULL DEFAULT 0,
15
+ "category_id" text NOT NULL,
16
+ "is_standard" boolean NOT NULL DEFAULT false,
17
+ "created_at" timestamptz NOT NULL DEFAULT now(),
18
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
19
+ "deleted_at" timestamptz NULL,
20
+ CONSTRAINT "category_custom_attribute_pkey" PRIMARY KEY ("id")
21
+ );
22
+ `);
23
+ this.addSql(`
24
+ CREATE INDEX IF NOT EXISTS "IDX_category_custom_attribute_category_id"
25
+ ON "category_custom_attribute" ("category_id")
26
+ WHERE "deleted_at" IS NULL;
27
+ `);
28
+ this.addSql(`
29
+ CREATE INDEX IF NOT EXISTS "IDX_category_custom_attribute_deleted_at"
30
+ ON "category_custom_attribute" ("deleted_at")
31
+ WHERE "deleted_at" IS NOT NULL;
32
+ `);
33
+ this.addSql(`
34
+ CREATE TABLE IF NOT EXISTS "product_custom_attribute" (
35
+ "id" text NOT NULL,
36
+ "product_id" text NOT NULL,
37
+ "value" text NOT NULL,
38
+ "value_numeric" integer NULL,
39
+ "value_file" text NULL,
40
+ "category_custom_attribute_id" text NOT NULL,
41
+ "created_at" timestamptz NOT NULL DEFAULT now(),
42
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
43
+ "deleted_at" timestamptz NULL,
44
+ CONSTRAINT "product_custom_attribute_pkey" PRIMARY KEY ("id"),
45
+ CONSTRAINT "product_custom_attribute_category_custom_attribute_id_foreign"
46
+ FOREIGN KEY ("category_custom_attribute_id")
47
+ REFERENCES "category_custom_attribute" ("id")
48
+ ON UPDATE CASCADE ON DELETE CASCADE
49
+ );
50
+ `);
51
+ this.addSql(`
52
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_product_id"
53
+ ON "product_custom_attribute" ("product_id")
54
+ WHERE "deleted_at" IS NULL;
55
+ `);
56
+ this.addSql(`
57
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_category_custom_attribute_id"
58
+ ON "product_custom_attribute" ("category_custom_attribute_id")
59
+ WHERE "deleted_at" IS NULL;
60
+ `);
61
+ this.addSql(`
62
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_deleted_at"
63
+ ON "product_custom_attribute" ("deleted_at")
64
+ WHERE "deleted_at" IS NOT NULL;
65
+ `);
66
+ }
67
+ async down() {
68
+ this.addSql(`DROP TABLE IF EXISTS "product_custom_attribute" CASCADE;`);
69
+ this.addSql(`DROP TABLE IF EXISTS "category_custom_attribute" CASCADE;`);
70
+ }
71
+ }
72
+ exports.Migration20260405120000 = Migration20260405120000;
73
+ //# sourceMappingURL=Migration20260405120000.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Migration20260405120000.js","sourceRoot":"","sources":["../../../../../../src/modules/product-attributes/migrations/Migration20260405120000.ts"],"names":[],"mappings":";;;AAAA,sDAAiD;AAEjD,MAAa,uBAAwB,SAAQ,sBAAS;IAC3C,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;KAiBX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,0DAA0D,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,2DAA2D,CAAC,CAAA;IAC1E,CAAC;CACF;AAzED,0DAyEC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empty-complete-org/medusa-product-attributes",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "Custom attributes module for Medusa v2 with support for text, number, file types and units of measurement",
5
5
  "author": "empty-complete",
6
6
  "license": "MIT",
@@ -63,6 +63,8 @@
63
63
  "@medusajs/js-sdk": "^2.13.1",
64
64
  "@medusajs/medusa": "^2.13.1",
65
65
  "@medusajs/ui": "^4.0.27",
66
+ "@mikro-orm/core": "6.4.16",
67
+ "@mikro-orm/migrations": "6.4.16",
66
68
  "@tanstack/react-query": "^5.64.2",
67
69
  "@types/jest": "^30.0.0",
68
70
  "@types/node": "^20.0.0",
@@ -71,6 +73,7 @@
71
73
  "jest": "^30.3.0",
72
74
  "react": "^19.0.0",
73
75
  "ts-jest": "^29.4.6",
76
+ "ts-node": "^10.9.2",
74
77
  "typescript": "^5.0.0",
75
78
  "yalc": "^1.0.0-pre.53"
76
79
  },
@@ -80,4 +83,4 @@
80
83
  "publishConfig": {
81
84
  "registry": "https://registry.npmjs.org"
82
85
  }
83
- }
86
+ }