@fayz-ai/shop 0.6.0 → 0.8.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/README.md +2 -0
- package/dist/index.cjs +84 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -27
- package/dist/index.js.map +1 -1
- package/dist/mock-provider.d.ts +6 -1
- package/dist/mock-provider.d.ts.map +1 -1
- package/dist/mock.cjs +46 -0
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.js +46 -0
- package/dist/mock.js.map +1 -1
- package/dist/provider.d.ts +9 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/runtime.cjs +46 -0
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +46 -0
- package/dist/runtime.js.map +1 -1
- package/dist/supabase-provider.d.ts.map +1 -1
- package/dist/tables.d.ts +10 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/index.ts +2 -1
- package/src/mock-provider.ts +46 -1
- package/src/provider.ts +12 -1
- package/src/supabase-provider.ts +30 -29
- package/src/tables.ts +12 -0
- package/src/types.ts +45 -0
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@fayz-ai/shop)
|
|
6
6
|
[](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
+
**Status:** beta — published to npm and used across Fayz dogfood apps. Pre-1.0: minor APIs may change before 1.0.
|
|
9
|
+
|
|
8
10
|
`@fayz-ai/shop` is the headless ecommerce engine: a `ShopProvider` contract plus the domain types for products, carts, orders, customers, discounts, and shipping. It owns *what commerce means* so the storefront (`@fayz-ai/storefront`) and admin surfaces only have to render it.
|
|
9
11
|
|
|
10
12
|
It's provider-first by design. Ship a demo on `createMockShopProvider`, then drop in `createSupabaseShopProvider` (or your own `ShopProvider`) for real data — the UI doesn't change. Tenancy is resolved through a pluggable resolver, so the same engine serves one store or a marketplace of thousands.
|
package/dist/index.cjs
CHANGED
|
@@ -178,6 +178,7 @@ function now() {
|
|
|
178
178
|
}
|
|
179
179
|
var ORDERS_KEY = "fayz.shop.mock.orders.v1";
|
|
180
180
|
var CUSTOMERS_KEY = "fayz.shop.mock.customers.v1";
|
|
181
|
+
var ENQUIRIES_KEY = "fayz.shop.mock.enquiries.v1";
|
|
181
182
|
function loadStored(key) {
|
|
182
183
|
try {
|
|
183
184
|
if (typeof localStorage === "undefined") return [];
|
|
@@ -198,6 +199,7 @@ var MockShopProvider = class {
|
|
|
198
199
|
constructor(seed) {
|
|
199
200
|
this.orders = loadStored(ORDERS_KEY);
|
|
200
201
|
this.customers = loadStored(CUSTOMERS_KEY);
|
|
202
|
+
this.enquiries = loadStored(ENQUIRIES_KEY);
|
|
201
203
|
this.products = (seed?.products ?? MOCK_PRODUCTS).map((p) => ({ ...p }));
|
|
202
204
|
this.categories = (seed?.categories ?? MOCK_CATEGORIES).map((c) => ({ ...c }));
|
|
203
205
|
this.discounts = (seed?.discounts ?? MOCK_DISCOUNTS).map((d) => ({ ...d }));
|
|
@@ -210,12 +212,18 @@ var MockShopProvider = class {
|
|
|
210
212
|
syncCustomers() {
|
|
211
213
|
this.customers = loadStored(CUSTOMERS_KEY);
|
|
212
214
|
}
|
|
215
|
+
syncEnquiries() {
|
|
216
|
+
this.enquiries = loadStored(ENQUIRIES_KEY);
|
|
217
|
+
}
|
|
213
218
|
persistOrders() {
|
|
214
219
|
store(ORDERS_KEY, this.orders);
|
|
215
220
|
}
|
|
216
221
|
persistCustomers() {
|
|
217
222
|
store(CUSTOMERS_KEY, this.customers);
|
|
218
223
|
}
|
|
224
|
+
persistEnquiries() {
|
|
225
|
+
store(ENQUIRIES_KEY, this.enquiries);
|
|
226
|
+
}
|
|
219
227
|
// ---------------------------------------------------------------------------
|
|
220
228
|
// Products
|
|
221
229
|
// ---------------------------------------------------------------------------
|
|
@@ -270,6 +278,44 @@ var MockShopProvider = class {
|
|
|
270
278
|
}
|
|
271
279
|
async deleteProductImage(_imageId) {
|
|
272
280
|
}
|
|
281
|
+
async createProductEnquiry(input) {
|
|
282
|
+
this.syncEnquiries();
|
|
283
|
+
const ts = now();
|
|
284
|
+
const enquiry = {
|
|
285
|
+
id: uid(),
|
|
286
|
+
tenantId: "mock",
|
|
287
|
+
productId: input.productId ?? null,
|
|
288
|
+
productName: input.productName,
|
|
289
|
+
productSlug: input.productSlug ?? null,
|
|
290
|
+
customerName: input.customerName,
|
|
291
|
+
customerEmail: input.customerEmail,
|
|
292
|
+
customerPhone: input.customerPhone ?? null,
|
|
293
|
+
subject: input.subject ?? null,
|
|
294
|
+
message: input.message,
|
|
295
|
+
sourceUrl: input.sourceUrl ?? null,
|
|
296
|
+
status: "new",
|
|
297
|
+
metadata: input.metadata ?? {},
|
|
298
|
+
createdAt: ts,
|
|
299
|
+
updatedAt: ts
|
|
300
|
+
};
|
|
301
|
+
this.enquiries.unshift(enquiry);
|
|
302
|
+
this.persistEnquiries();
|
|
303
|
+
return enquiry;
|
|
304
|
+
}
|
|
305
|
+
async listProductEnquiries(opts) {
|
|
306
|
+
this.syncEnquiries();
|
|
307
|
+
let list = [...this.enquiries];
|
|
308
|
+
if (opts?.productId) list = list.filter((e) => e.productId === opts.productId);
|
|
309
|
+
if (opts?.customerEmail) {
|
|
310
|
+
const email = opts.customerEmail.toLowerCase();
|
|
311
|
+
list = list.filter((e) => e.customerEmail.toLowerCase() === email);
|
|
312
|
+
}
|
|
313
|
+
if (opts?.status) list = list.filter((e) => e.status === opts.status);
|
|
314
|
+
list.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
315
|
+
const offset = opts?.offset ?? 0;
|
|
316
|
+
if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : void 0);
|
|
317
|
+
return list;
|
|
318
|
+
}
|
|
273
319
|
// ---------------------------------------------------------------------------
|
|
274
320
|
// Categories
|
|
275
321
|
// ---------------------------------------------------------------------------
|
|
@@ -605,6 +651,17 @@ function getShopTenantId() {
|
|
|
605
651
|
}
|
|
606
652
|
}
|
|
607
653
|
|
|
654
|
+
// src/tables.ts
|
|
655
|
+
var T = {
|
|
656
|
+
products: "plg_shop_products",
|
|
657
|
+
productImages: "plg_shop_product_images",
|
|
658
|
+
categories: "plg_shop_categories",
|
|
659
|
+
orders: "plg_shop_orders",
|
|
660
|
+
orderItems: "plg_shop_order_items",
|
|
661
|
+
customers: "plg_shop_customers",
|
|
662
|
+
discounts: "plg_shop_discounts"
|
|
663
|
+
};
|
|
664
|
+
|
|
608
665
|
// src/supabase-provider.ts
|
|
609
666
|
function getDb() {
|
|
610
667
|
const supabase = core.getSupabaseClientOptional();
|
|
@@ -732,7 +789,7 @@ var SupabaseShopProvider = class {
|
|
|
732
789
|
async listProducts(options) {
|
|
733
790
|
const db = getDb();
|
|
734
791
|
const tenantId = getTenantId();
|
|
735
|
-
let q = db.from(
|
|
792
|
+
let q = db.from(T.products).select(`*, images:${T.productImages}(*)`);
|
|
736
793
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
737
794
|
if (options?.status) q = q.eq("status", options.status);
|
|
738
795
|
if (options?.categoryId) q = q.eq("category_id", options.categoryId);
|
|
@@ -747,14 +804,14 @@ var SupabaseShopProvider = class {
|
|
|
747
804
|
}
|
|
748
805
|
async getProduct(id) {
|
|
749
806
|
const db = getDb();
|
|
750
|
-
const { data, error } = await db.from(
|
|
807
|
+
const { data, error } = await db.from(T.products).select(`*, images:${T.productImages}(*)`).eq("id", id).maybeSingle();
|
|
751
808
|
if (error) throw error;
|
|
752
809
|
return data ? rowToProduct(data) : null;
|
|
753
810
|
}
|
|
754
811
|
async createProduct(input) {
|
|
755
812
|
const db = getDb();
|
|
756
813
|
const tenantId = getTenantId();
|
|
757
|
-
const { data, error } = await db.from(
|
|
814
|
+
const { data, error } = await db.from(T.products).insert({
|
|
758
815
|
tenant_id: tenantId,
|
|
759
816
|
name: input.name,
|
|
760
817
|
slug: input.slug ?? slugify(input.name),
|
|
@@ -771,7 +828,7 @@ var SupabaseShopProvider = class {
|
|
|
771
828
|
is_physical: input.isPhysical ?? true,
|
|
772
829
|
weight: input.weight ?? null,
|
|
773
830
|
weight_unit: input.weightUnit ?? "kg"
|
|
774
|
-
}).select(
|
|
831
|
+
}).select(`*, images:${T.productImages}(*)`).single();
|
|
775
832
|
if (error) throw error;
|
|
776
833
|
return rowToProduct(data);
|
|
777
834
|
}
|
|
@@ -793,12 +850,12 @@ var SupabaseShopProvider = class {
|
|
|
793
850
|
if (input.categoryId !== void 0) updates.category_id = input.categoryId;
|
|
794
851
|
if (input.isPhysical !== void 0) updates.is_physical = input.isPhysical;
|
|
795
852
|
if (input.weight !== void 0) updates.weight = input.weight;
|
|
796
|
-
const { data, error } = await db.from(
|
|
853
|
+
const { data, error } = await db.from(T.products).update(updates).eq("id", id).select(`*, images:${T.productImages}(*)`).single();
|
|
797
854
|
if (error) throw error;
|
|
798
855
|
return rowToProduct(data);
|
|
799
856
|
}
|
|
800
857
|
async deleteProduct(id) {
|
|
801
|
-
const { error } = await getDb().from(
|
|
858
|
+
const { error } = await getDb().from(T.products).delete().eq("id", id);
|
|
802
859
|
if (error) throw error;
|
|
803
860
|
}
|
|
804
861
|
async uploadProductImage(productId, file) {
|
|
@@ -808,12 +865,12 @@ var SupabaseShopProvider = class {
|
|
|
808
865
|
const { error: uploadErr } = await db.storage.from("shop-images").upload(path, file);
|
|
809
866
|
if (uploadErr) throw uploadErr;
|
|
810
867
|
const { data: { publicUrl } } = db.storage.from("shop-images").getPublicUrl(path);
|
|
811
|
-
const { data, error } = await db.from(
|
|
868
|
+
const { data, error } = await db.from(T.productImages).insert({ product_id: productId, tenant_id: tenantId, url: publicUrl, sort_order: 0, is_primary: false }).select().single();
|
|
812
869
|
if (error) throw error;
|
|
813
870
|
return rowToImage(data);
|
|
814
871
|
}
|
|
815
872
|
async deleteProductImage(imageId) {
|
|
816
|
-
const { error } = await getDb().from(
|
|
873
|
+
const { error } = await getDb().from(T.productImages).delete().eq("id", imageId);
|
|
817
874
|
if (error) throw error;
|
|
818
875
|
}
|
|
819
876
|
// ---------------------------------------------------------------------------
|
|
@@ -822,7 +879,7 @@ var SupabaseShopProvider = class {
|
|
|
822
879
|
async listCategories() {
|
|
823
880
|
const db = getDb();
|
|
824
881
|
const tenantId = getTenantId();
|
|
825
|
-
let q = db.from(
|
|
882
|
+
let q = db.from(T.categories).select("*").order("sort_order", { ascending: true });
|
|
826
883
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
827
884
|
const { data, error } = await q;
|
|
828
885
|
if (error) throw error;
|
|
@@ -830,7 +887,7 @@ var SupabaseShopProvider = class {
|
|
|
830
887
|
}
|
|
831
888
|
async createCategory(input) {
|
|
832
889
|
const db = getDb();
|
|
833
|
-
const { data, error } = await db.from(
|
|
890
|
+
const { data, error } = await db.from(T.categories).insert({ tenant_id: getTenantId(), name: input.name, slug: input.slug ?? slugify(input.name), description: input.description ?? null, parent_id: input.parentId ?? null, sort_order: input.sortOrder ?? 0 }).select().single();
|
|
834
891
|
if (error) throw error;
|
|
835
892
|
return rowToCategory(data);
|
|
836
893
|
}
|
|
@@ -842,12 +899,12 @@ var SupabaseShopProvider = class {
|
|
|
842
899
|
if (input.description !== void 0) updates.description = input.description;
|
|
843
900
|
if (input.parentId !== void 0) updates.parent_id = input.parentId;
|
|
844
901
|
if (input.sortOrder !== void 0) updates.sort_order = input.sortOrder;
|
|
845
|
-
const { data, error } = await db.from(
|
|
902
|
+
const { data, error } = await db.from(T.categories).update(updates).eq("id", id).select().single();
|
|
846
903
|
if (error) throw error;
|
|
847
904
|
return rowToCategory(data);
|
|
848
905
|
}
|
|
849
906
|
async deleteCategory(id) {
|
|
850
|
-
const { error } = await getDb().from(
|
|
907
|
+
const { error } = await getDb().from(T.categories).delete().eq("id", id);
|
|
851
908
|
if (error) throw error;
|
|
852
909
|
}
|
|
853
910
|
// ---------------------------------------------------------------------------
|
|
@@ -856,7 +913,7 @@ var SupabaseShopProvider = class {
|
|
|
856
913
|
async listOrders(options) {
|
|
857
914
|
const db = getDb();
|
|
858
915
|
const tenantId = getTenantId();
|
|
859
|
-
let q = db.from(
|
|
916
|
+
let q = db.from(T.orders).select(`*, items:${T.orderItems}(*)`);
|
|
860
917
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
861
918
|
if (options?.status) q = q.eq("status", options.status);
|
|
862
919
|
if (options?.financialStatus) q = q.eq("financial_status", options.financialStatus);
|
|
@@ -871,7 +928,7 @@ var SupabaseShopProvider = class {
|
|
|
871
928
|
return (data ?? []).map(rowToOrder);
|
|
872
929
|
}
|
|
873
930
|
async getOrder(id) {
|
|
874
|
-
const { data, error } = await getDb().from(
|
|
931
|
+
const { data, error } = await getDb().from(T.orders).select(`*, items:${T.orderItems}(*)`).eq("id", id).maybeSingle();
|
|
875
932
|
if (error) throw error;
|
|
876
933
|
return data ? rowToOrder(data) : null;
|
|
877
934
|
}
|
|
@@ -882,10 +939,10 @@ var SupabaseShopProvider = class {
|
|
|
882
939
|
const discountTotal = input.discountTotal ?? 0;
|
|
883
940
|
const shippingTotal = input.shippingTotal ?? 0;
|
|
884
941
|
const total = Math.round((subtotal - discountTotal + shippingTotal) * 100) / 100;
|
|
885
|
-
const { data: order, error: orderErr } = await db.from(
|
|
942
|
+
const { data: order, error: orderErr } = await db.from(T.orders).insert({ tenant_id: tenantId, customer_id: input.customerId ?? null, customer_name: input.customerName ?? null, customer_email: input.customerEmail ?? null, currency: input.currency ?? "BRL", notes: input.notes ?? null, subtotal, discount_total: discountTotal, shipping_total: shippingTotal, discount_code: input.discountCode ?? null, total, status: "open", financial_status: "pending", fulfillment_status: "unfulfilled" }).select().single();
|
|
886
943
|
if (orderErr) throw orderErr;
|
|
887
944
|
const items = input.items.map((i) => ({ order_id: order.id, product_id: asUuid(i.productId), name: i.name, sku: i.sku ?? null, quantity: i.quantity, unit_price: i.unitPrice, total: i.quantity * i.unitPrice, image_url: i.imageUrl ?? null }));
|
|
888
|
-
await db.from(
|
|
945
|
+
await db.from(T.orderItems).insert(items);
|
|
889
946
|
return this.getOrder(order.id);
|
|
890
947
|
}
|
|
891
948
|
async updateOrder(id, input) {
|
|
@@ -894,7 +951,7 @@ var SupabaseShopProvider = class {
|
|
|
894
951
|
if (input.financialStatus !== void 0) updates.financial_status = input.financialStatus;
|
|
895
952
|
if (input.fulfillmentStatus !== void 0) updates.fulfillment_status = input.fulfillmentStatus;
|
|
896
953
|
if (input.notes !== void 0) updates.notes = input.notes;
|
|
897
|
-
const { error } = await getDb().from(
|
|
954
|
+
const { error } = await getDb().from(T.orders).update(updates).eq("id", id);
|
|
898
955
|
if (error) throw error;
|
|
899
956
|
return this.getOrder(id);
|
|
900
957
|
}
|
|
@@ -927,7 +984,7 @@ var SupabaseShopProvider = class {
|
|
|
927
984
|
async listCustomers(options) {
|
|
928
985
|
const db = getDb();
|
|
929
986
|
const tenantId = getTenantId();
|
|
930
|
-
let q = db.from(
|
|
987
|
+
let q = db.from(T.customers).select("*").order("first_name", { ascending: true });
|
|
931
988
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
932
989
|
if (options?.search) q = q.or(`first_name.ilike.%${options.search}%,last_name.ilike.%${options.search}%,email.ilike.%${options.search}%`);
|
|
933
990
|
if (options?.limit) q = q.limit(options.limit);
|
|
@@ -936,12 +993,12 @@ var SupabaseShopProvider = class {
|
|
|
936
993
|
return (data ?? []).map(rowToCustomer);
|
|
937
994
|
}
|
|
938
995
|
async getCustomer(id) {
|
|
939
|
-
const { data, error } = await getDb().from(
|
|
996
|
+
const { data, error } = await getDb().from(T.customers).select("*").eq("id", id).maybeSingle();
|
|
940
997
|
if (error) throw error;
|
|
941
998
|
return data ? rowToCustomer(data) : null;
|
|
942
999
|
}
|
|
943
1000
|
async createCustomer(input) {
|
|
944
|
-
const { data, error } = await getDb().from(
|
|
1001
|
+
const { data, error } = await getDb().from(T.customers).insert({ tenant_id: getTenantId(), first_name: input.firstName, last_name: input.lastName ?? "", email: input.email ?? null, phone: input.phone ?? null, notes: input.notes ?? null }).select().single();
|
|
945
1002
|
if (error) throw error;
|
|
946
1003
|
return rowToCustomer(data);
|
|
947
1004
|
}
|
|
@@ -952,12 +1009,12 @@ var SupabaseShopProvider = class {
|
|
|
952
1009
|
if (input.email !== void 0) updates.email = input.email;
|
|
953
1010
|
if (input.phone !== void 0) updates.phone = input.phone;
|
|
954
1011
|
if (input.notes !== void 0) updates.notes = input.notes;
|
|
955
|
-
const { data, error } = await getDb().from(
|
|
1012
|
+
const { data, error } = await getDb().from(T.customers).update(updates).eq("id", id).select().single();
|
|
956
1013
|
if (error) throw error;
|
|
957
1014
|
return rowToCustomer(data);
|
|
958
1015
|
}
|
|
959
1016
|
async deleteCustomer(id) {
|
|
960
|
-
const { error } = await getDb().from(
|
|
1017
|
+
const { error } = await getDb().from(T.customers).delete().eq("id", id);
|
|
961
1018
|
if (error) throw error;
|
|
962
1019
|
}
|
|
963
1020
|
// Find-or-create + link to auth.uid server-side (SECURITY DEFINER RPC), so the
|
|
@@ -982,7 +1039,7 @@ var SupabaseShopProvider = class {
|
|
|
982
1039
|
async listDiscounts(options) {
|
|
983
1040
|
const db = getDb();
|
|
984
1041
|
const tenantId = getTenantId();
|
|
985
|
-
let q = db.from(
|
|
1042
|
+
let q = db.from(T.discounts).select("*").order("created_at", { ascending: false });
|
|
986
1043
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
987
1044
|
if (options?.status) q = q.eq("status", options.status);
|
|
988
1045
|
if (options?.type) q = q.eq("type", options.type);
|
|
@@ -993,12 +1050,12 @@ var SupabaseShopProvider = class {
|
|
|
993
1050
|
return (data ?? []).map(rowToDiscount);
|
|
994
1051
|
}
|
|
995
1052
|
async getDiscount(id) {
|
|
996
|
-
const { data, error } = await getDb().from(
|
|
1053
|
+
const { data, error } = await getDb().from(T.discounts).select("*").eq("id", id).maybeSingle();
|
|
997
1054
|
if (error) throw error;
|
|
998
1055
|
return data ? rowToDiscount(data) : null;
|
|
999
1056
|
}
|
|
1000
1057
|
async createDiscount(input) {
|
|
1001
|
-
const { data, error } = await getDb().from(
|
|
1058
|
+
const { data, error } = await getDb().from(T.discounts).insert({ tenant_id: getTenantId(), title: input.title, code: input.code ?? null, type: input.type, method: input.method ?? "code", value: input.value, usage_limit: input.usageLimit ?? null, once_per_customer: input.oncePerCustomer ?? false, starts_at: input.startsAt ?? (/* @__PURE__ */ new Date()).toISOString(), ends_at: input.endsAt ?? null, status: input.status ?? "active", times_used: 0 }).select().single();
|
|
1002
1059
|
if (error) throw error;
|
|
1003
1060
|
return rowToDiscount(data);
|
|
1004
1061
|
}
|
|
@@ -1011,12 +1068,12 @@ var SupabaseShopProvider = class {
|
|
|
1011
1068
|
if (input.usageLimit !== void 0) updates.usage_limit = input.usageLimit;
|
|
1012
1069
|
if (input.status !== void 0) updates.status = input.status;
|
|
1013
1070
|
if (input.endsAt !== void 0) updates.ends_at = input.endsAt;
|
|
1014
|
-
const { data, error } = await getDb().from(
|
|
1071
|
+
const { data, error } = await getDb().from(T.discounts).update(updates).eq("id", id).select().single();
|
|
1015
1072
|
if (error) throw error;
|
|
1016
1073
|
return rowToDiscount(data);
|
|
1017
1074
|
}
|
|
1018
1075
|
async deleteDiscount(id) {
|
|
1019
|
-
const { error } = await getDb().from(
|
|
1076
|
+
const { error } = await getDb().from(T.discounts).delete().eq("id", id);
|
|
1020
1077
|
if (error) throw error;
|
|
1021
1078
|
}
|
|
1022
1079
|
};
|