@axova/shared 1.0.2 → 1.1.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 (68) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2 -0
  3. package/dist/lib/db.d.ts +34406 -1
  4. package/dist/lib/db.js +21 -1
  5. package/dist/middleware/storeOwnership.js +22 -3
  6. package/dist/middleware/storeValidationMiddleware.js +16 -39
  7. package/dist/schemas/admin/admin-schema.d.ts +2 -2
  8. package/dist/schemas/ai-moderation/ai-moderation-schema.d.ts +6 -6
  9. package/dist/schemas/common/common-schemas.d.ts +71 -71
  10. package/dist/schemas/compliance/compliance-schema.d.ts +20 -20
  11. package/dist/schemas/compliance/kyc-schema.d.ts +8 -8
  12. package/dist/schemas/customer/customer-schema.d.ts +18 -18
  13. package/dist/schemas/index.d.ts +28 -0
  14. package/dist/schemas/index.js +134 -3
  15. package/dist/schemas/inventory/inventory-tables.d.ts +188 -188
  16. package/dist/schemas/inventory/lot-tables.d.ts +102 -102
  17. package/dist/schemas/order/cart-schema.d.ts +2865 -0
  18. package/dist/schemas/order/cart-schema.js +396 -0
  19. package/dist/schemas/order/order-schema.d.ts +19 -19
  20. package/dist/schemas/order/order-schema.js +8 -2
  21. package/dist/schemas/product/discount-schema.d.ts +3 -3
  22. package/dist/schemas/product/product-schema.d.ts +3 -3
  23. package/dist/schemas/store/store-audit-schema.d.ts +20 -20
  24. package/dist/schemas/store/store-schema.d.ts +182 -2
  25. package/dist/schemas/store/store-schema.js +19 -0
  26. package/dist/schemas/store/storefront-config-schema.d.ts +434 -823
  27. package/dist/schemas/store/storefront-config-schema.js +35 -62
  28. package/dist/utils/subdomain.d.ts +1 -1
  29. package/dist/utils/subdomain.js +10 -15
  30. package/package.json +1 -1
  31. package/src/configs/index.ts +654 -654
  32. package/src/index.ts +26 -23
  33. package/src/interfaces/customer-events.ts +106 -106
  34. package/src/interfaces/inventory-events.ts +545 -545
  35. package/src/interfaces/inventory-types.ts +1004 -1004
  36. package/src/interfaces/order-events.ts +381 -381
  37. package/src/lib/auditLogger.ts +1117 -1117
  38. package/src/lib/authOrganization.ts +153 -153
  39. package/src/lib/db.ts +84 -64
  40. package/src/middleware/serviceAuth.ts +328 -328
  41. package/src/middleware/storeOwnership.ts +199 -181
  42. package/src/middleware/storeValidationMiddleware.ts +17 -50
  43. package/src/middleware/userAuth.ts +248 -248
  44. package/src/schemas/admin/admin-schema.ts +208 -208
  45. package/src/schemas/ai-moderation/ai-moderation-schema.ts +180 -180
  46. package/src/schemas/common/common-schemas.ts +108 -108
  47. package/src/schemas/compliance/compliance-schema.ts +927 -0
  48. package/src/schemas/compliance/kyc-schema.ts +649 -0
  49. package/src/schemas/customer/customer-schema.ts +576 -0
  50. package/src/schemas/index.ts +202 -3
  51. package/src/schemas/inventory/inventory-tables.ts +1927 -0
  52. package/src/schemas/inventory/lot-tables.ts +799 -0
  53. package/src/schemas/order/cart-schema.ts +652 -0
  54. package/src/schemas/order/order-schema.ts +1406 -0
  55. package/src/schemas/product/discount-relations.ts +44 -0
  56. package/src/schemas/product/discount-schema.ts +464 -0
  57. package/src/schemas/product/product-relations.ts +187 -0
  58. package/src/schemas/product/product-schema.ts +955 -0
  59. package/src/schemas/store/ethiopian_business_api.md.resolved +212 -0
  60. package/src/schemas/store/store-audit-schema.ts +1257 -0
  61. package/src/schemas/store/store-schema.ts +682 -0
  62. package/src/schemas/store/store-settings-schema.ts +231 -0
  63. package/src/schemas/store/storefront-config-schema.ts +382 -0
  64. package/src/schemas/types.ts +67 -67
  65. package/src/types/events.ts +646 -646
  66. package/src/utils/errorHandler.ts +44 -44
  67. package/src/utils/subdomain.ts +19 -23
  68. package/tsconfig.json +21 -21
@@ -0,0 +1,682 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+ import { relations } from "drizzle-orm";
3
+ import {
4
+ boolean,
5
+ index,
6
+ integer,
7
+ jsonb,
8
+ pgTable,
9
+ text,
10
+ timestamp,
11
+ unique,
12
+ varchar,
13
+ numeric,
14
+ } from "drizzle-orm/pg-core";
15
+
16
+ // Core Store Table - Enhanced with performance optimizations
17
+ export const stores = pgTable(
18
+ "stores",
19
+ {
20
+ id: text("id")
21
+ .primaryKey()
22
+ .$defaultFn(() => createId()),
23
+ userId: text("user_id").notNull(),
24
+ organizationId: text("organization_id"), // Organization ID from Fastify Better Auth
25
+ storeName: varchar("store_name", { length: 255 }).notNull(),
26
+ subdomain: varchar("subdomain", { length: 63 }).notNull(),
27
+ storeUrl: varchar("store_url", { length: 255 }),
28
+
29
+ // Core Settings
30
+ timezone: varchar("timezone", { length: 50 }).notNull().default("UTC"),
31
+ currency: varchar("currency", { length: 3 }).notNull().default("USD"),
32
+ languages: jsonb("languages").$type<string[]>().notNull().default(["en"]),
33
+
34
+ // Store Configuration
35
+ storeType: varchar("store_type", { length: 50 })
36
+ .notNull()
37
+ .default("STANDARD")
38
+ .$type<"STANDARD" | "PREMIUM" | "ENTERPRISE">(),
39
+
40
+ plan: varchar("plan", { length: 50 })
41
+ .notNull()
42
+ .default("FREE")
43
+ .$type<"FREE" | "BASIC" | "PRO" | "ENTERPRISE">(),
44
+
45
+ // Ethiopian Business Registration
46
+ tinNumber: varchar("tin_number", { length: 50 }),
47
+ businessType: varchar("business_type", { length: 20 })
48
+ .$type<"starter" | "business_owner">()
49
+ .default("starter"),
50
+ region: varchar("region", { length: 100 }),
51
+ subcity: varchar("subcity", { length: 100 }),
52
+ woreda: varchar("woreda", { length: 100 }),
53
+ houseNumber: varchar("house_number", { length: 50 }),
54
+ tradeNames: jsonb("trade_names").$type<string[]>().default([]),
55
+
56
+ // TIN Verification Status Tracking
57
+ tinVerificationStatus: varchar("tin_verification_status", { length: 30 })
58
+ .$type<"UNVERIFIED" | "PENDING" | "VERIFIED" | "FAILED" | "EXPIRED">()
59
+ .default("UNVERIFIED"),
60
+ tinVerifiedAt: timestamp("tin_verified_at", { withTimezone: true }),
61
+ tinVerifiedBy: text("tin_verified_by"),
62
+ tinVerificationData: jsonb("tin_verification_data")
63
+ .$type<Record<string, unknown>>()
64
+ .default({}),
65
+ tinVerificationErrors: jsonb("tin_verification_errors")
66
+ .$type<string[]>()
67
+ .default([]),
68
+ tinLastChecked: timestamp("tin_last_checked", { withTimezone: true }),
69
+
70
+ // Business Type Change Tracking
71
+ businessTypeChangedAt: timestamp("business_type_changed_at", { withTimezone: true }),
72
+ businessTypeChangedBy: text("business_type_changed_by"),
73
+ businessTypeChangeReason: text("business_type_change_reason"),
74
+ previousBusinessType: varchar("previous_business_type", { length: 20 })
75
+ .$type<"starter" | "business_owner">(),
76
+
77
+ // Store Additional Details
78
+ attributes: jsonb("attributes")
79
+ .$type<Record<string, unknown>>()
80
+ .default({}),
81
+ settings: jsonb("settings")
82
+ .$type<{
83
+ enablePublicListing?: boolean;
84
+ enableReviews?: boolean;
85
+ enableInventoryTracking?: boolean;
86
+ enableMultiCurrency?: boolean;
87
+ maintenanceMode?: boolean;
88
+ customCss?: string;
89
+ logoUrl?: string;
90
+ faviconUrl?: string;
91
+ themeColor?: string;
92
+ }>()
93
+ .default({}),
94
+
95
+ // Performance metrics
96
+ monthlyVisitors: integer("monthly_visitors").default(0),
97
+ totalOrders: integer("total_orders").default(0),
98
+ totalRevenue: integer("total_revenue").default(0), // in cents
99
+
100
+ // Status and lifecycle
101
+ isActive: boolean("is_active").notNull().default(true),
102
+ isPublished: boolean("is_published").notNull().default(false),
103
+ launchedAt: timestamp("launched_at", { withTimezone: true }),
104
+
105
+ // Audit fields
106
+ createdAt: timestamp("created_at", { withTimezone: true })
107
+ .defaultNow()
108
+ .notNull(),
109
+ updatedAt: timestamp("updated_at", { withTimezone: true })
110
+ .defaultNow()
111
+ .notNull(),
112
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
113
+ },
114
+ (table) => ({
115
+ // Primary indexes for performance
116
+ subdomainUnique: unique("idx_stores_subdomain_unique").on(table.subdomain),
117
+ storeNameIndex: index("idx_stores_name").on(table.storeName),
118
+ userIdIndex: index("idx_stores_user_id").on(table.userId),
119
+ organizationIdIndex: index("idx_stores_organization_id").on(
120
+ table.organizationId,
121
+ ),
122
+
123
+ // Composite indexes for common queries
124
+ userActiveStoresIndex: index("idx_stores_user_active").on(
125
+ table.userId,
126
+ table.isActive,
127
+ ),
128
+ activePublishedIndex: index("idx_stores_active_published").on(
129
+ table.isActive,
130
+ table.isPublished,
131
+ ),
132
+ planTypeIndex: index("idx_stores_plan_type").on(
133
+ table.plan,
134
+ table.storeType,
135
+ ),
136
+
137
+ // Performance tracking indexes
138
+ visitorsIndex: index("idx_stores_monthly_visitors").on(
139
+ table.monthlyVisitors,
140
+ ),
141
+ revenueIndex: index("idx_stores_total_revenue").on(table.totalRevenue),
142
+
143
+ // Temporal indexes
144
+ createdAtIndex: index("idx_stores_created_at").on(table.createdAt),
145
+ launchedAtIndex: index("idx_stores_launched_at").on(table.launchedAt),
146
+
147
+ // Ethiopian business indexes
148
+ tinNumberIndex: index("idx_stores_tin_number").on(table.tinNumber),
149
+ businessTypeIndex: index("idx_stores_business_type").on(table.businessType),
150
+ locationIndex: index("idx_stores_location").on(
151
+ table.region,
152
+ table.subcity,
153
+ table.woreda,
154
+ ),
155
+ }),
156
+ );
157
+
158
+ // Store Business Details Table - Enhanced
159
+ export const storeBusinessDetails = pgTable(
160
+ "store_business_details",
161
+ {
162
+ id: text("id")
163
+ .primaryKey()
164
+ .$defaultFn(() => createId()),
165
+ storeId: text("store_id")
166
+ .notNull()
167
+ .references(() => stores.id, { onDelete: "cascade" }),
168
+
169
+ // Business Type and Legal Structure
170
+ businessType: varchar("business_type", { length: 20 })
171
+ .notNull()
172
+ .$type<"INDIVIDUAL" | "BUSINESS" | "NON_PROFIT" | "GOVERNMENT">()
173
+ .default("INDIVIDUAL"),
174
+
175
+ legalStructure: varchar("legal_structure", { length: 30 }).$type<
176
+ | "SOLE_PROPRIETORSHIP"
177
+ | "PARTNERSHIP"
178
+ | "LLC"
179
+ | "CORPORATION"
180
+ | "S_CORP"
181
+ | "NON_PROFIT"
182
+ >(),
183
+
184
+ // Ethiopian Registration Details
185
+ legalBusinessNameAmh: varchar("legal_business_name_amh", { length: 255 }),
186
+ registrationDate: timestamp("registration_date", { withTimezone: true }),
187
+ paidUpCapital: numeric("paid_up_capital", { precision: 15, scale: 2 }),
188
+ licenseDetails: jsonb("license_details").$type<any[]>().default([]),
189
+ associateInfo: jsonb("associate_info").$type<any[]>().default([]),
190
+ tradeNames: jsonb("trade_names").$type<string[]>().default([]),
191
+
192
+ // Business Details
193
+ businessName: varchar("business_name", { length: 255 }),
194
+ dbaName: varchar("dba_name", { length: 255 }), // "Doing Business As"
195
+ businessRegistrationNumber: varchar("business_registration_number", {
196
+ length: 100,
197
+ }),
198
+ taxIdentificationNumber: varchar("tax_identification_number", {
199
+ length: 100,
200
+ }),
201
+ vatNumber: varchar("vat_number", { length: 50 }),
202
+
203
+ // Address Information
204
+ businessAddress: jsonb("business_address").$type<{
205
+ street: string;
206
+ street2?: string;
207
+ city: string;
208
+ state: string;
209
+ postalCode: string;
210
+ country: string;
211
+ }>(),
212
+
213
+ mailingAddress: jsonb("mailing_address").$type<{
214
+ street: string;
215
+ street2?: string;
216
+ city: string;
217
+ state: string;
218
+ postalCode: string;
219
+ country: string;
220
+ sameAsBusiness?: boolean;
221
+ }>(),
222
+
223
+ // Industry and Size
224
+ industry: varchar("industry", { length: 100 }),
225
+ subIndustry: varchar("sub_industry", { length: 100 }),
226
+ employeeCount: varchar("employee_count", { length: 20 }).$type<
227
+ "1" | "2-10" | "11-50" | "51-200" | "201-500" | "500+"
228
+ >(),
229
+
230
+ // Financial Information
231
+ annualRevenue: varchar("annual_revenue", { length: 30 }).$type<
232
+ | "<10K"
233
+ | "10K-50K"
234
+ | "50K-100K"
235
+ | "100K-500K"
236
+ | "500K-1M"
237
+ | "1M-10M"
238
+ | "10M+"
239
+ >(),
240
+
241
+ // Verification Status
242
+ isVerified: boolean("is_verified").notNull().default(false),
243
+ verificationStatus: varchar("verification_status", { length: 30 })
244
+ .notNull()
245
+ .default("PENDING")
246
+ .$type<
247
+ "PENDING" | "IN_PROGRESS" | "VERIFIED" | "REJECTED" | "REQUIRES_UPDATE"
248
+ >(),
249
+
250
+ verificationDocuments: jsonb("verification_documents").$type<{
251
+ businessLicense?: string;
252
+ taxCertificate?: string;
253
+ bankStatement?: string;
254
+ proofOfAddress?: string;
255
+ other?: string[];
256
+ }>(),
257
+
258
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
259
+ verifiedBy: text("verified_by"),
260
+
261
+ createdAt: timestamp("created_at", { withTimezone: true })
262
+ .defaultNow()
263
+ .notNull(),
264
+ updatedAt: timestamp("updated_at", { withTimezone: true })
265
+ .defaultNow()
266
+ .notNull(),
267
+ },
268
+ (table) => ({
269
+ storeIdIndex: index("idx_business_details_store_id").on(table.storeId),
270
+ businessTypeIndex: index("idx_business_details_type").on(
271
+ table.businessType,
272
+ ),
273
+ verificationStatusIndex: index("idx_business_details_verification").on(
274
+ table.verificationStatus,
275
+ ),
276
+ industryIndex: index("idx_business_details_industry").on(table.industry),
277
+
278
+ // Composite indexes for verification workflows
279
+ verificationWorkflowIndex: index("idx_business_verification_workflow").on(
280
+ table.verificationStatus,
281
+ table.businessType,
282
+ ),
283
+
284
+ // Ethiopian indexes
285
+ tinIndex: index("idx_business_details_tin").on(table.taxIdentificationNumber),
286
+ registrationDateIndex: index("idx_business_details_registration_date").on(table.registrationDate),
287
+ }),
288
+ );
289
+
290
+ // Store Contacts Table - Enhanced
291
+ export const storeContacts = pgTable(
292
+ "store_contacts",
293
+ {
294
+ id: text("id")
295
+ .primaryKey()
296
+ .$defaultFn(() => createId()),
297
+ storeId: text("store_id")
298
+ .notNull()
299
+ .references(() => stores.id, { onDelete: "cascade" }),
300
+
301
+ // Primary Contact Information
302
+ contactEmail: varchar("contact_email", { length: 255 }).notNull(),
303
+ contactName: varchar("contact_name", { length: 255 }),
304
+ contactTitle: varchar("contact_title", { length: 100 }),
305
+
306
+ // Departmental Emails
307
+ supportEmail: varchar("support_email", { length: 255 }),
308
+ salesEmail: varchar("sales_email", { length: 255 }),
309
+ technicalEmail: varchar("technical_email", { length: 255 }),
310
+ billingEmail: varchar("billing_email", { length: 255 }),
311
+ marketingEmail: varchar("marketing_email", { length: 255 }),
312
+ legalEmail: varchar("legal_email", { length: 255 }),
313
+
314
+ // Phone Numbers with international support
315
+ primaryPhone: varchar("primary_phone", { length: 20 }),
316
+ supportPhone: varchar("support_phone", { length: 20 }),
317
+ whatsappNumber: varchar("whatsapp_number", { length: 20 }),
318
+ faxNumber: varchar("fax_number", { length: 20 }),
319
+
320
+ // Business Hours
321
+ businessHours: jsonb("business_hours").$type<{
322
+ timezone: string;
323
+ monday?: { open: string; close: string; closed?: boolean };
324
+ tuesday?: { open: string; close: string; closed?: boolean };
325
+ wednesday?: { open: string; close: string; closed?: boolean };
326
+ thursday?: { open: string; close: string; closed?: boolean };
327
+ friday?: { open: string; close: string; closed?: boolean };
328
+ saturday?: { open: string; close: string; closed?: boolean };
329
+ sunday?: { open: string; close: string; closed?: boolean };
330
+ holidays?: Array<{ date: string; name: string }>;
331
+ }>(),
332
+
333
+ // Emergency and After-hours Contact
334
+ emergencyContact: jsonb("emergency_contact").$type<{
335
+ name: string;
336
+ position: string;
337
+ phone: string;
338
+ email: string;
339
+ available24x7: boolean;
340
+ escalationProcess?: string;
341
+ }>(),
342
+
343
+ // Communication Preferences
344
+ preferredContactMethod: varchar("preferred_contact_method", { length: 20 })
345
+ .$type<"EMAIL" | "PHONE" | "SMS" | "WHATSAPP">()
346
+ .default("EMAIL"),
347
+
348
+ // Social Media and Other Channels
349
+ communicationChannels: jsonb("communication_channels").$type<{
350
+ slack?: string;
351
+ discord?: string;
352
+ telegram?: string;
353
+ skype?: string;
354
+ zoom?: string;
355
+ teams?: string;
356
+ }>(),
357
+
358
+ createdAt: timestamp("created_at", { withTimezone: true })
359
+ .defaultNow()
360
+ .notNull(),
361
+ updatedAt: timestamp("updated_at", { withTimezone: true })
362
+ .defaultNow()
363
+ .notNull(),
364
+ },
365
+ (table) => ({
366
+ storeIdIndex: index("idx_contacts_store_id").on(table.storeId),
367
+ contactEmailIndex: index("idx_contacts_email").on(table.contactEmail),
368
+ preferredMethodIndex: index("idx_contacts_preferred_method").on(
369
+ table.preferredContactMethod,
370
+ ),
371
+ }),
372
+ );
373
+
374
+ // Store Social Profiles Table - Enhanced
375
+ export const storeSocialProfiles = pgTable(
376
+ "store_social_profiles",
377
+ {
378
+ id: text("id")
379
+ .primaryKey()
380
+ .$defaultFn(() => createId()),
381
+ storeId: text("store_id")
382
+ .notNull()
383
+ .references(() => stores.id, { onDelete: "cascade" }),
384
+
385
+ // Major Social Platforms
386
+ facebookUrl: varchar("facebook_url", { length: 500 }),
387
+ facebookEnabled: boolean("facebook_enabled").notNull().default(false),
388
+ facebookPageId: varchar("facebook_page_id", { length: 100 }),
389
+
390
+ instagramUrl: varchar("instagram_url", { length: 500 }),
391
+ instagramEnabled: boolean("instagram_enabled").notNull().default(false),
392
+ instagramHandle: varchar("instagram_handle", { length: 100 }),
393
+
394
+ twitterUrl: varchar("twitter_url", { length: 500 }),
395
+ twitterEnabled: boolean("twitter_enabled").notNull().default(false),
396
+ twitterHandle: varchar("twitter_handle", { length: 100 }),
397
+
398
+ linkedinUrl: varchar("linkedin_url", { length: 500 }),
399
+ linkedinEnabled: boolean("linkedin_enabled").notNull().default(false),
400
+ linkedinPageId: varchar("linkedin_page_id", { length: 100 }),
401
+
402
+ youtubeUrl: varchar("youtube_url", { length: 500 }),
403
+ youtubeEnabled: boolean("youtube_enabled").notNull().default(false),
404
+ youtubeChannelId: varchar("youtube_channel_id", { length: 100 }),
405
+
406
+ tiktokUrl: varchar("tiktok_url", { length: 500 }),
407
+ tiktokEnabled: boolean("tiktok_enabled").notNull().default(false),
408
+ tiktokHandle: varchar("tiktok_handle", { length: 100 }),
409
+
410
+ pinterestUrl: varchar("pinterest_url", { length: 500 }),
411
+ pinterestEnabled: boolean("pinterest_enabled").notNull().default(false),
412
+ pinterestHandle: varchar("pinterest_handle", { length: 100 }),
413
+
414
+ // Professional and Niche Platforms
415
+ githubUrl: varchar("github_url", { length: 500 }),
416
+ githubEnabled: boolean("github_enabled").notNull().default(false),
417
+
418
+ snapchatUrl: varchar("snapchat_url", { length: 500 }),
419
+ snapchatEnabled: boolean("snapchat_enabled").notNull().default(false),
420
+
421
+ redditUrl: varchar("reddit_url", { length: 500 }),
422
+ redditEnabled: boolean("reddit_enabled").notNull().default(false),
423
+
424
+ // Messaging Platforms
425
+ discordUrl: varchar("discord_url", { length: 500 }),
426
+ discordEnabled: boolean("discord_enabled").notNull().default(false),
427
+
428
+ telegramUrl: varchar("telegram_url", { length: 500 }),
429
+ telegramEnabled: boolean("telegram_enabled").notNull().default(false),
430
+
431
+ whatsappUrl: varchar("whatsapp_url", { length: 500 }),
432
+ whatsappEnabled: boolean("whatsapp_enabled").notNull().default(false),
433
+
434
+ // Custom and Regional Platforms
435
+ customPlatforms: jsonb("custom_platforms")
436
+ .$type<
437
+ Array<{
438
+ name: string;
439
+ url: string;
440
+ enabled: boolean;
441
+ icon?: string;
442
+ displayOrder?: number;
443
+ }>
444
+ >()
445
+ .default([]),
446
+
447
+ // Social Media Strategy
448
+ socialMediaStrategy: jsonb("social_media_strategy").$type<{
449
+ primaryPlatforms: string[];
450
+ postingSchedule?: Record<string, string[]>; // platform -> days/times
451
+ contentThemes?: string[];
452
+ hashtagStrategy?: Record<string, string[]>; // platform -> hashtags
453
+ engagementGoals?: Record<string, number>;
454
+ }>(),
455
+
456
+ // Analytics and Tracking
457
+ socialAnalytics: jsonb("social_analytics")
458
+ .$type<{
459
+ trackingEnabled: boolean;
460
+ utmSource?: string;
461
+ utmMedium?: string;
462
+ utmCampaign?: string;
463
+ conversionTracking?: boolean;
464
+ }>()
465
+ .default({ trackingEnabled: false }),
466
+
467
+ createdAt: timestamp("created_at", { withTimezone: true })
468
+ .defaultNow()
469
+ .notNull(),
470
+ updatedAt: timestamp("updated_at", { withTimezone: true })
471
+ .defaultNow()
472
+ .notNull(),
473
+ },
474
+ (table) => ({
475
+ storeIdIndex: index("idx_social_store_id").on(table.storeId),
476
+ // Index for enabled platforms for quick filtering
477
+ enabledPlatformsIndex: index("idx_social_enabled_facebook").on(
478
+ table.storeId,
479
+ table.facebookEnabled,
480
+ ),
481
+ enabledInstagramIndex: index("idx_social_enabled_instagram").on(
482
+ table.storeId,
483
+ table.instagramEnabled,
484
+ ),
485
+ enabledTwitterIndex: index("idx_social_enabled_twitter").on(
486
+ table.storeId,
487
+ table.twitterEnabled,
488
+ ),
489
+ }),
490
+ );
491
+
492
+ // Store Blogs Table - Enhanced
493
+ export const storeBlogs = pgTable(
494
+ "store_blogs",
495
+ {
496
+ id: text("id")
497
+ .primaryKey()
498
+ .$defaultFn(() => createId()),
499
+ storeId: text("store_id")
500
+ .notNull()
501
+ .references(() => stores.id, { onDelete: "cascade" }),
502
+
503
+ // Content
504
+ title: varchar("title", { length: 255 }).notNull(),
505
+ slug: varchar("slug", { length: 255 }).notNull(),
506
+ excerpt: text("excerpt"),
507
+ content: text("content").notNull(),
508
+
509
+ // Media
510
+ featuredImage: varchar("featured_image", { length: 500 }),
511
+ gallery: jsonb("gallery").$type<string[]>().default([]),
512
+ media: jsonb("media")
513
+ .$type<
514
+ Array<{
515
+ type:
516
+ | "video"
517
+ | "audio"
518
+ | "podcast"
519
+ | "youtube"
520
+ | "vimeo"
521
+ | "spotify"
522
+ | "soundcloud"
523
+ | "other";
524
+ url: string;
525
+ title?: string;
526
+ description?: string;
527
+ thumbnail?: string;
528
+ duration?: number; // Duration in seconds
529
+ size?: number; // File size in bytes
530
+ }>
531
+ >()
532
+ .default([]),
533
+
534
+ // SEO and Metadata
535
+ metaTitle: varchar("meta_title", { length: 255 }),
536
+ metaDescription: text("meta_description"),
537
+ metaKeywords: jsonb("meta_keywords").$type<string[]>().default([]),
538
+ canonicalUrl: varchar("canonical_url", { length: 500 }),
539
+
540
+ // Content Organization
541
+ categoryId: text("category_id"),
542
+ tags: jsonb("tags").$type<string[]>().notNull().default([]),
543
+ topics: jsonb("topics").$type<string[]>().default([]),
544
+
545
+ // Publishing
546
+ status: varchar("status", { length: 20 })
547
+ .notNull()
548
+ .default("draft")
549
+ .$type<"draft" | "pending" | "published" | "archived" | "deleted">(),
550
+
551
+ publishedAt: timestamp("published_at", { withTimezone: true }),
552
+ scheduledFor: timestamp("scheduled_for", { withTimezone: true }),
553
+
554
+ // Authoring
555
+ authors: jsonb("authors")
556
+ .$type<
557
+ Array<{
558
+ id: string;
559
+ name: string;
560
+ role?: string;
561
+ bio?: string;
562
+ avatar?: string;
563
+ }>
564
+ >()
565
+ .notNull()
566
+ .default([]),
567
+
568
+ // Engagement and Analytics
569
+ viewCount: integer("view_count").default(0),
570
+ likeCount: integer("like_count").default(0),
571
+ shareCount: integer("share_count").default(0),
572
+ commentCount: integer("comment_count").default(0),
573
+
574
+ // Content Settings
575
+ allowComments: boolean("allow_comments").notNull().default(true),
576
+ allowSharing: boolean("allow_sharing").notNull().default(true),
577
+ allowRating: boolean("allow_rating").notNull().default(false),
578
+
579
+ // Features
580
+ isFeatured: boolean("is_featured").notNull().default(false),
581
+ isSticky: boolean("is_sticky").notNull().default(false),
582
+ isPremium: boolean("is_premium").notNull().default(false),
583
+
584
+ // Reading Experience
585
+ estimatedReadTime: integer("estimated_read_time"), // in minutes
586
+ difficulty: varchar("difficulty", { length: 20 }).$type<
587
+ "beginner" | "intermediate" | "advanced"
588
+ >(),
589
+
590
+ // Language and Localization
591
+ language: varchar("language", { length: 10 }).default("en"),
592
+ translations: jsonb("translations")
593
+ .$type<Record<string, string>>()
594
+ .default({}),
595
+
596
+ createdAt: timestamp("created_at", { withTimezone: true })
597
+ .defaultNow()
598
+ .notNull(),
599
+ updatedAt: timestamp("updated_at", { withTimezone: true })
600
+ .defaultNow()
601
+ .notNull(),
602
+ },
603
+ (table) => ({
604
+ storeIdIndex: index("idx_blog_store_id").on(table.storeId),
605
+ slugUniqueIndex: unique("idx_blog_slug_unique").on(
606
+ table.storeId,
607
+ table.slug,
608
+ ),
609
+ statusIndex: index("idx_blog_status").on(table.status),
610
+ publishedAtIndex: index("idx_blog_published_at").on(table.publishedAt),
611
+
612
+ // Content discovery indexes
613
+ featuredIndex: index("idx_blog_featured").on(
614
+ table.isFeatured,
615
+ table.status,
616
+ ),
617
+ categoryIndex: index("idx_blog_category").on(
618
+ table.categoryId,
619
+ table.status,
620
+ ),
621
+
622
+ // Performance indexes
623
+ viewCountIndex: index("idx_blog_view_count").on(table.viewCount),
624
+
625
+ // Publishing workflow indexes
626
+ publishingWorkflowIndex: index("idx_blog_publishing_workflow").on(
627
+ table.status,
628
+ table.scheduledFor,
629
+ ),
630
+ }),
631
+ );
632
+
633
+ // Relations
634
+ export const storesRelations = relations(stores, ({ one, many }) => ({
635
+ businessDetails: one(storeBusinessDetails, {
636
+ fields: [stores.id],
637
+ references: [storeBusinessDetails.storeId],
638
+ }),
639
+ contacts: one(storeContacts, {
640
+ fields: [stores.id],
641
+ references: [storeContacts.storeId],
642
+ }),
643
+ socialProfiles: one(storeSocialProfiles, {
644
+ fields: [stores.id],
645
+ references: [storeSocialProfiles.storeId],
646
+ }),
647
+ blogs: many(storeBlogs),
648
+ }));
649
+
650
+ export const storeBusinessDetailsRelations = relations(
651
+ storeBusinessDetails,
652
+ ({ one }) => ({
653
+ store: one(stores, {
654
+ fields: [storeBusinessDetails.storeId],
655
+ references: [stores.id],
656
+ }),
657
+ }),
658
+ );
659
+
660
+ export const storeContactsRelations = relations(storeContacts, ({ one }) => ({
661
+ store: one(stores, {
662
+ fields: [storeContacts.storeId],
663
+ references: [stores.id],
664
+ }),
665
+ }));
666
+
667
+ export const storeSocialProfilesRelations = relations(
668
+ storeSocialProfiles,
669
+ ({ one }) => ({
670
+ store: one(stores, {
671
+ fields: [storeSocialProfiles.storeId],
672
+ references: [stores.id],
673
+ }),
674
+ }),
675
+ );
676
+
677
+ export const storeBlogsRelations = relations(storeBlogs, ({ one }) => ({
678
+ store: one(stores, {
679
+ fields: [storeBlogs.storeId],
680
+ references: [stores.id],
681
+ }),
682
+ }));