@l4yercak3/cli 1.2.15 → 1.2.18

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.
@@ -0,0 +1,1132 @@
1
+ /**
2
+ * Supabase Database Generator
3
+ * Generates Supabase schema, migrations, and client for L4YERCAK3 integration
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { ensureDir, writeFileWithBackup, checkFileOverwrite } = require('../../../utils/file-utils');
9
+
10
+ class SupabaseGenerator {
11
+ /**
12
+ * Generate Supabase database files
13
+ * @param {Object} options - Generation options
14
+ * @returns {Promise<Object>} - Generated file paths
15
+ */
16
+ async generate(options) {
17
+ const { projectPath, features = [] } = options;
18
+
19
+ const results = {
20
+ schema: null,
21
+ client: null,
22
+ types: null,
23
+ middleware: null,
24
+ };
25
+
26
+ const supabaseDir = path.join(projectPath, 'supabase');
27
+ const migrationsDir = path.join(supabaseDir, 'migrations');
28
+ ensureDir(supabaseDir);
29
+ ensureDir(migrationsDir);
30
+
31
+ // Generate initial migration
32
+ results.schema = await this.generateMigration(migrationsDir, features);
33
+
34
+ // Generate Supabase client
35
+ results.client = await this.generateClient(projectPath, options);
36
+
37
+ // Generate TypeScript types
38
+ if (options.isTypeScript) {
39
+ results.types = await this.generateTypes(projectPath);
40
+ }
41
+
42
+ // Generate middleware for auth
43
+ if (features.includes('oauth')) {
44
+ results.middleware = await this.generateMiddleware(projectPath, options);
45
+ }
46
+
47
+ return results;
48
+ }
49
+
50
+ async generateMigration(migrationsDir, _features) {
51
+ const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
52
+ const outputPath = path.join(migrationsDir, `${timestamp}_l4yercak3_schema.sql`);
53
+
54
+ const action = await checkFileOverwrite(outputPath);
55
+ if (action === 'skip') {
56
+ return null;
57
+ }
58
+
59
+ const content = `-- L4YERCAK3 Integration Schema
60
+ -- Auto-generated by @l4yercak3/cli
61
+ --
62
+ -- This schema follows the ontology-first pattern, mirroring
63
+ -- L4YERCAK3's universal object structure for seamless sync.
64
+
65
+ -- Enable required extensions
66
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
67
+
68
+ -- ============================================
69
+ -- ONTOLOGY: Universal object storage
70
+ -- ============================================
71
+
72
+ CREATE TYPE sync_status AS ENUM (
73
+ 'synced',
74
+ 'pending_push',
75
+ 'pending_pull',
76
+ 'conflict',
77
+ 'local_only'
78
+ );
79
+
80
+ CREATE TABLE objects (
81
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
82
+ l4yercak3_id TEXT UNIQUE,
83
+ organization_id TEXT NOT NULL,
84
+
85
+ -- Core fields
86
+ type TEXT NOT NULL,
87
+ subtype TEXT,
88
+ name TEXT NOT NULL,
89
+ status TEXT NOT NULL,
90
+
91
+ -- Type-specific data
92
+ custom_properties JSONB DEFAULT '{}',
93
+
94
+ -- Sync tracking
95
+ sync_status sync_status DEFAULT 'local_only',
96
+ synced_at TIMESTAMPTZ,
97
+ local_version INTEGER DEFAULT 1,
98
+ remote_version INTEGER,
99
+
100
+ -- Timestamps
101
+ created_at TIMESTAMPTZ DEFAULT NOW(),
102
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
103
+ deleted_at TIMESTAMPTZ
104
+ );
105
+
106
+ -- Indexes for objects
107
+ CREATE INDEX idx_objects_l4yercak3_id ON objects(l4yercak3_id);
108
+ CREATE INDEX idx_objects_type ON objects(type);
109
+ CREATE INDEX idx_objects_type_status ON objects(type, status);
110
+ CREATE INDEX idx_objects_type_subtype ON objects(type, subtype);
111
+ CREATE INDEX idx_objects_sync_status ON objects(sync_status);
112
+ CREATE INDEX idx_objects_organization ON objects(organization_id);
113
+ CREATE INDEX idx_objects_updated ON objects(updated_at DESC);
114
+
115
+ -- Object relationships
116
+ CREATE TABLE object_links (
117
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
118
+ l4yercak3_id TEXT,
119
+ from_object_id UUID REFERENCES objects(id) ON DELETE CASCADE,
120
+ to_object_id UUID REFERENCES objects(id) ON DELETE CASCADE,
121
+ link_type TEXT NOT NULL,
122
+ metadata JSONB,
123
+ sync_status TEXT DEFAULT 'local_only',
124
+ created_at TIMESTAMPTZ DEFAULT NOW()
125
+ );
126
+
127
+ CREATE INDEX idx_object_links_from ON object_links(from_object_id);
128
+ CREATE INDEX idx_object_links_to ON object_links(to_object_id);
129
+ CREATE INDEX idx_object_links_from_type ON object_links(from_object_id, link_type);
130
+
131
+ -- ============================================
132
+ -- AUTHENTICATION: Local user management
133
+ -- ============================================
134
+
135
+ CREATE TABLE frontend_users (
136
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
137
+ l4yercak3_contact_id TEXT,
138
+ l4yercak3_frontend_user_id TEXT,
139
+ organization_id TEXT NOT NULL,
140
+
141
+ -- Core identity
142
+ email TEXT UNIQUE NOT NULL,
143
+ email_verified BOOLEAN DEFAULT FALSE,
144
+ name TEXT,
145
+ first_name TEXT,
146
+ last_name TEXT,
147
+ image TEXT,
148
+ phone TEXT,
149
+
150
+ -- Local auth
151
+ password_hash TEXT,
152
+
153
+ -- OAuth accounts stored as JSONB array
154
+ oauth_accounts JSONB DEFAULT '[]',
155
+
156
+ -- App-specific
157
+ role TEXT DEFAULT 'user',
158
+ preferences JSONB DEFAULT '{}',
159
+
160
+ -- Sync
161
+ sync_status TEXT DEFAULT 'pending_push',
162
+ synced_at TIMESTAMPTZ,
163
+
164
+ -- Timestamps
165
+ created_at TIMESTAMPTZ DEFAULT NOW(),
166
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
167
+ last_login_at TIMESTAMPTZ
168
+ );
169
+
170
+ CREATE INDEX idx_frontend_users_email ON frontend_users(email);
171
+ CREATE INDEX idx_frontend_users_l4yercak3_contact ON frontend_users(l4yercak3_contact_id);
172
+ CREATE INDEX idx_frontend_users_organization ON frontend_users(organization_id);
173
+
174
+ -- Sessions
175
+ CREATE TABLE sessions (
176
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
177
+ user_id UUID REFERENCES frontend_users(id) ON DELETE CASCADE,
178
+ session_token TEXT UNIQUE NOT NULL,
179
+ expires_at TIMESTAMPTZ NOT NULL,
180
+ user_agent TEXT,
181
+ ip_address TEXT,
182
+ created_at TIMESTAMPTZ DEFAULT NOW()
183
+ );
184
+
185
+ CREATE INDEX idx_sessions_token ON sessions(session_token);
186
+ CREATE INDEX idx_sessions_user ON sessions(user_id);
187
+
188
+ -- ============================================
189
+ -- STRIPE: Local payment handling
190
+ -- ============================================
191
+
192
+ CREATE TABLE stripe_customers (
193
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
194
+ frontend_user_id UUID REFERENCES frontend_users(id) ON DELETE CASCADE,
195
+ stripe_customer_id TEXT UNIQUE NOT NULL,
196
+ l4yercak3_contact_id TEXT,
197
+ email TEXT NOT NULL,
198
+ name TEXT,
199
+ default_payment_method_id TEXT,
200
+ sync_status TEXT DEFAULT 'local_only',
201
+ created_at TIMESTAMPTZ DEFAULT NOW()
202
+ );
203
+
204
+ CREATE INDEX idx_stripe_customers_stripe_id ON stripe_customers(stripe_customer_id);
205
+ CREATE INDEX idx_stripe_customers_user ON stripe_customers(frontend_user_id);
206
+
207
+ CREATE TABLE stripe_payments (
208
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
209
+ stripe_payment_intent_id TEXT UNIQUE NOT NULL,
210
+ stripe_customer_id TEXT,
211
+ frontend_user_id UUID REFERENCES frontend_users(id),
212
+
213
+ -- Payment details
214
+ amount INTEGER NOT NULL,
215
+ currency TEXT NOT NULL,
216
+ status TEXT NOT NULL,
217
+ payment_method TEXT,
218
+
219
+ -- What was purchased
220
+ metadata JSONB NOT NULL,
221
+
222
+ -- L4YERCAK3 sync
223
+ l4yercak3_order_id TEXT,
224
+ l4yercak3_invoice_id TEXT,
225
+ sync_status TEXT DEFAULT 'pending_push',
226
+ synced_at TIMESTAMPTZ,
227
+
228
+ -- Timestamps
229
+ created_at TIMESTAMPTZ DEFAULT NOW(),
230
+ completed_at TIMESTAMPTZ
231
+ );
232
+
233
+ CREATE INDEX idx_stripe_payments_stripe_id ON stripe_payments(stripe_payment_intent_id);
234
+ CREATE INDEX idx_stripe_payments_customer ON stripe_payments(stripe_customer_id);
235
+ CREATE INDEX idx_stripe_payments_user ON stripe_payments(frontend_user_id);
236
+ CREATE INDEX idx_stripe_payments_sync ON stripe_payments(sync_status);
237
+
238
+ CREATE TABLE stripe_subscriptions (
239
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
240
+ stripe_subscription_id TEXT UNIQUE NOT NULL,
241
+ stripe_customer_id TEXT NOT NULL,
242
+ frontend_user_id UUID REFERENCES frontend_users(id),
243
+
244
+ status TEXT NOT NULL,
245
+ price_id TEXT NOT NULL,
246
+ product_id TEXT NOT NULL,
247
+
248
+ current_period_start TIMESTAMPTZ NOT NULL,
249
+ current_period_end TIMESTAMPTZ NOT NULL,
250
+ cancel_at_period_end BOOLEAN DEFAULT FALSE,
251
+
252
+ l4yercak3_subscription_id TEXT,
253
+ sync_status TEXT DEFAULT 'local_only',
254
+
255
+ created_at TIMESTAMPTZ DEFAULT NOW(),
256
+ updated_at TIMESTAMPTZ DEFAULT NOW()
257
+ );
258
+
259
+ CREATE INDEX idx_stripe_subscriptions_stripe_id ON stripe_subscriptions(stripe_subscription_id);
260
+ CREATE INDEX idx_stripe_subscriptions_user ON stripe_subscriptions(frontend_user_id);
261
+
262
+ -- ============================================
263
+ -- SYNC: Job tracking
264
+ -- ============================================
265
+
266
+ CREATE TYPE sync_direction AS ENUM ('push', 'pull', 'bidirectional');
267
+ CREATE TYPE sync_job_status AS ENUM ('pending', 'running', 'completed', 'failed');
268
+
269
+ CREATE TABLE sync_jobs (
270
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
271
+ entity_type TEXT NOT NULL,
272
+ direction sync_direction NOT NULL,
273
+ status sync_job_status DEFAULT 'pending',
274
+
275
+ cursor TEXT,
276
+ processed_count INTEGER DEFAULT 0,
277
+ total_count INTEGER,
278
+
279
+ error_message TEXT,
280
+ error_details JSONB,
281
+
282
+ started_at TIMESTAMPTZ DEFAULT NOW(),
283
+ completed_at TIMESTAMPTZ
284
+ );
285
+
286
+ CREATE INDEX idx_sync_jobs_status ON sync_jobs(status);
287
+ CREATE INDEX idx_sync_jobs_entity ON sync_jobs(entity_type);
288
+
289
+ CREATE TABLE sync_conflicts (
290
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
291
+ object_id UUID REFERENCES objects(id) ON DELETE CASCADE,
292
+ local_version JSONB,
293
+ remote_version JSONB,
294
+ conflict_type TEXT NOT NULL,
295
+ resolved_at TIMESTAMPTZ,
296
+ resolution TEXT,
297
+ created_at TIMESTAMPTZ DEFAULT NOW()
298
+ );
299
+
300
+ CREATE INDEX idx_sync_conflicts_object ON sync_conflicts(object_id);
301
+ CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(resolved_at) WHERE resolved_at IS NULL;
302
+
303
+ -- ============================================
304
+ -- TRIGGERS: Auto-update timestamps
305
+ -- ============================================
306
+
307
+ CREATE OR REPLACE FUNCTION update_updated_at()
308
+ RETURNS TRIGGER AS $$
309
+ BEGIN
310
+ NEW.updated_at = NOW();
311
+ RETURN NEW;
312
+ END;
313
+ $$ LANGUAGE plpgsql;
314
+
315
+ CREATE TRIGGER objects_updated_at
316
+ BEFORE UPDATE ON objects
317
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
318
+
319
+ CREATE TRIGGER frontend_users_updated_at
320
+ BEFORE UPDATE ON frontend_users
321
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
322
+
323
+ CREATE TRIGGER stripe_subscriptions_updated_at
324
+ BEFORE UPDATE ON stripe_subscriptions
325
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
326
+
327
+ -- ============================================
328
+ -- ROW LEVEL SECURITY
329
+ -- ============================================
330
+
331
+ ALTER TABLE objects ENABLE ROW LEVEL SECURITY;
332
+ ALTER TABLE object_links ENABLE ROW LEVEL SECURITY;
333
+ ALTER TABLE frontend_users ENABLE ROW LEVEL SECURITY;
334
+ ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
335
+ ALTER TABLE stripe_customers ENABLE ROW LEVEL SECURITY;
336
+ ALTER TABLE stripe_payments ENABLE ROW LEVEL SECURITY;
337
+ ALTER TABLE stripe_subscriptions ENABLE ROW LEVEL SECURITY;
338
+
339
+ -- Objects: users can read/write their own organization's objects
340
+ CREATE POLICY objects_select ON objects
341
+ FOR SELECT USING (true); -- Adjust based on your auth requirements
342
+
343
+ CREATE POLICY objects_insert ON objects
344
+ FOR INSERT WITH CHECK (true);
345
+
346
+ CREATE POLICY objects_update ON objects
347
+ FOR UPDATE USING (true);
348
+
349
+ CREATE POLICY objects_delete ON objects
350
+ FOR DELETE USING (true);
351
+
352
+ -- Frontend users: users can read/update their own profile
353
+ CREATE POLICY frontend_users_select ON frontend_users
354
+ FOR SELECT USING (true);
355
+
356
+ CREATE POLICY frontend_users_update ON frontend_users
357
+ FOR UPDATE USING (auth.uid()::text = id::text);
358
+
359
+ -- Sessions: users can manage their own sessions
360
+ CREATE POLICY sessions_all ON sessions
361
+ FOR ALL USING (true);
362
+
363
+ -- Stripe: users can view their own payment data
364
+ CREATE POLICY stripe_customers_select ON stripe_customers
365
+ FOR SELECT USING (true);
366
+
367
+ CREATE POLICY stripe_payments_select ON stripe_payments
368
+ FOR SELECT USING (true);
369
+
370
+ CREATE POLICY stripe_subscriptions_select ON stripe_subscriptions
371
+ FOR SELECT USING (true);
372
+ `;
373
+
374
+ return writeFileWithBackup(outputPath, content, action);
375
+ }
376
+
377
+ async generateClient(projectPath, options) {
378
+ const { isTypeScript } = options;
379
+
380
+ // Determine output directory
381
+ let outputDir;
382
+ if (fs.existsSync(path.join(projectPath, 'src'))) {
383
+ outputDir = path.join(projectPath, 'src', 'lib', 'supabase');
384
+ } else {
385
+ outputDir = path.join(projectPath, 'lib', 'supabase');
386
+ }
387
+
388
+ ensureDir(outputDir);
389
+
390
+ const extension = isTypeScript ? 'ts' : 'js';
391
+ const outputPath = path.join(outputDir, `client.${extension}`);
392
+
393
+ const action = await checkFileOverwrite(outputPath);
394
+ if (action === 'skip') {
395
+ return null;
396
+ }
397
+
398
+ const content = isTypeScript
399
+ ? this.generateTypeScriptClient()
400
+ : this.generateJavaScriptClient();
401
+
402
+ return writeFileWithBackup(outputPath, content, action);
403
+ }
404
+
405
+ generateTypeScriptClient() {
406
+ return `/**
407
+ * Supabase Client
408
+ * Auto-generated by @l4yercak3/cli
409
+ */
410
+
411
+ import { createClient } from '@supabase/supabase-js';
412
+ import type { Database } from './types';
413
+
414
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
415
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
416
+
417
+ if (!supabaseUrl || !supabaseAnonKey) {
418
+ throw new Error('Missing Supabase environment variables');
419
+ }
420
+
421
+ // Client-side Supabase client
422
+ export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
423
+
424
+ // Server-side Supabase client (with service role key)
425
+ export function createServerClient() {
426
+ const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
427
+ if (!serviceKey) {
428
+ throw new Error('SUPABASE_SERVICE_ROLE_KEY is required for server operations');
429
+ }
430
+ return createClient<Database>(supabaseUrl, serviceKey);
431
+ }
432
+
433
+ // ============ Object Helpers ============
434
+
435
+ export async function getObjects(type: string, options?: {
436
+ status?: string;
437
+ subtype?: string;
438
+ limit?: number;
439
+ }) {
440
+ let query = supabase
441
+ .from('objects')
442
+ .select('*')
443
+ .eq('type', type)
444
+ .is('deleted_at', null)
445
+ .order('updated_at', { ascending: false });
446
+
447
+ if (options?.status) {
448
+ query = query.eq('status', options.status);
449
+ }
450
+ if (options?.subtype) {
451
+ query = query.eq('subtype', options.subtype);
452
+ }
453
+ if (options?.limit) {
454
+ query = query.limit(options.limit);
455
+ }
456
+
457
+ return query;
458
+ }
459
+
460
+ export async function getObject(id: string) {
461
+ return supabase
462
+ .from('objects')
463
+ .select('*')
464
+ .eq('id', id)
465
+ .single();
466
+ }
467
+
468
+ export async function createObject(data: {
469
+ type: string;
470
+ name: string;
471
+ status: string;
472
+ subtype?: string;
473
+ custom_properties?: Record<string, unknown>;
474
+ organization_id: string;
475
+ }) {
476
+ return supabase
477
+ .from('objects')
478
+ .insert({
479
+ ...data,
480
+ sync_status: 'local_only',
481
+ local_version: 1,
482
+ })
483
+ .select()
484
+ .single();
485
+ }
486
+
487
+ export async function updateObject(id: string, data: Partial<{
488
+ name: string;
489
+ status: string;
490
+ subtype: string;
491
+ custom_properties: Record<string, unknown>;
492
+ }>) {
493
+ // First get the current object to increment version
494
+ const { data: existing } = await supabase
495
+ .from('objects')
496
+ .select('local_version, sync_status')
497
+ .eq('id', id)
498
+ .single();
499
+
500
+ const updates: Record<string, unknown> = {
501
+ ...data,
502
+ local_version: (existing?.local_version || 0) + 1,
503
+ };
504
+
505
+ // Mark as pending push if previously synced
506
+ if (existing?.sync_status === 'synced') {
507
+ updates.sync_status = 'pending_push';
508
+ }
509
+
510
+ return supabase
511
+ .from('objects')
512
+ .update(updates)
513
+ .eq('id', id)
514
+ .select()
515
+ .single();
516
+ }
517
+
518
+ export async function deleteObject(id: string) {
519
+ // Soft delete
520
+ return supabase
521
+ .from('objects')
522
+ .update({
523
+ deleted_at: new Date().toISOString(),
524
+ sync_status: 'pending_push',
525
+ })
526
+ .eq('id', id);
527
+ }
528
+
529
+ // ============ User Helpers ============
530
+
531
+ export async function getUserByEmail(email: string) {
532
+ return supabase
533
+ .from('frontend_users')
534
+ .select('*')
535
+ .eq('email', email)
536
+ .single();
537
+ }
538
+
539
+ export async function createUser(data: {
540
+ email: string;
541
+ name?: string;
542
+ first_name?: string;
543
+ last_name?: string;
544
+ organization_id: string;
545
+ role?: string;
546
+ }) {
547
+ return supabase
548
+ .from('frontend_users')
549
+ .insert({
550
+ ...data,
551
+ email_verified: false,
552
+ oauth_accounts: [],
553
+ role: data.role || 'user',
554
+ sync_status: 'pending_push',
555
+ })
556
+ .select()
557
+ .single();
558
+ }
559
+
560
+ // ============ Sync Helpers ============
561
+
562
+ export async function getPendingSync(direction: 'push' | 'pull') {
563
+ const status = direction === 'push' ? 'pending_push' : 'pending_pull';
564
+ return supabase
565
+ .from('objects')
566
+ .select('*')
567
+ .eq('sync_status', status);
568
+ }
569
+
570
+ export async function markSynced(id: string, l4yercak3Id: string) {
571
+ return supabase
572
+ .from('objects')
573
+ .update({
574
+ l4yercak3_id: l4yercak3Id,
575
+ sync_status: 'synced',
576
+ synced_at: new Date().toISOString(),
577
+ })
578
+ .eq('id', id);
579
+ }
580
+ `;
581
+ }
582
+
583
+ generateJavaScriptClient() {
584
+ return `/**
585
+ * Supabase Client
586
+ * Auto-generated by @l4yercak3/cli
587
+ */
588
+
589
+ const { createClient } = require('@supabase/supabase-js');
590
+
591
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
592
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
593
+
594
+ if (!supabaseUrl || !supabaseAnonKey) {
595
+ throw new Error('Missing Supabase environment variables');
596
+ }
597
+
598
+ // Client-side Supabase client
599
+ const supabase = createClient(supabaseUrl, supabaseAnonKey);
600
+
601
+ // Server-side Supabase client
602
+ function createServerClient() {
603
+ const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
604
+ if (!serviceKey) {
605
+ throw new Error('SUPABASE_SERVICE_ROLE_KEY is required for server operations');
606
+ }
607
+ return createClient(supabaseUrl, serviceKey);
608
+ }
609
+
610
+ // ============ Object Helpers ============
611
+
612
+ async function getObjects(type, options = {}) {
613
+ let query = supabase
614
+ .from('objects')
615
+ .select('*')
616
+ .eq('type', type)
617
+ .is('deleted_at', null)
618
+ .order('updated_at', { ascending: false });
619
+
620
+ if (options.status) {
621
+ query = query.eq('status', options.status);
622
+ }
623
+ if (options.subtype) {
624
+ query = query.eq('subtype', options.subtype);
625
+ }
626
+ if (options.limit) {
627
+ query = query.limit(options.limit);
628
+ }
629
+
630
+ return query;
631
+ }
632
+
633
+ async function getObject(id) {
634
+ return supabase
635
+ .from('objects')
636
+ .select('*')
637
+ .eq('id', id)
638
+ .single();
639
+ }
640
+
641
+ async function createObject(data) {
642
+ return supabase
643
+ .from('objects')
644
+ .insert({
645
+ ...data,
646
+ sync_status: 'local_only',
647
+ local_version: 1,
648
+ })
649
+ .select()
650
+ .single();
651
+ }
652
+
653
+ async function updateObject(id, data) {
654
+ const { data: existing } = await supabase
655
+ .from('objects')
656
+ .select('local_version, sync_status')
657
+ .eq('id', id)
658
+ .single();
659
+
660
+ const updates = {
661
+ ...data,
662
+ local_version: (existing?.local_version || 0) + 1,
663
+ };
664
+
665
+ if (existing?.sync_status === 'synced') {
666
+ updates.sync_status = 'pending_push';
667
+ }
668
+
669
+ return supabase
670
+ .from('objects')
671
+ .update(updates)
672
+ .eq('id', id)
673
+ .select()
674
+ .single();
675
+ }
676
+
677
+ async function deleteObject(id) {
678
+ return supabase
679
+ .from('objects')
680
+ .update({
681
+ deleted_at: new Date().toISOString(),
682
+ sync_status: 'pending_push',
683
+ })
684
+ .eq('id', id);
685
+ }
686
+
687
+ // ============ User Helpers ============
688
+
689
+ async function getUserByEmail(email) {
690
+ return supabase
691
+ .from('frontend_users')
692
+ .select('*')
693
+ .eq('email', email)
694
+ .single();
695
+ }
696
+
697
+ async function createUser(data) {
698
+ return supabase
699
+ .from('frontend_users')
700
+ .insert({
701
+ ...data,
702
+ email_verified: false,
703
+ oauth_accounts: [],
704
+ role: data.role || 'user',
705
+ sync_status: 'pending_push',
706
+ })
707
+ .select()
708
+ .single();
709
+ }
710
+
711
+ // ============ Sync Helpers ============
712
+
713
+ async function getPendingSync(direction) {
714
+ const status = direction === 'push' ? 'pending_push' : 'pending_pull';
715
+ return supabase
716
+ .from('objects')
717
+ .select('*')
718
+ .eq('sync_status', status);
719
+ }
720
+
721
+ async function markSynced(id, l4yercak3Id) {
722
+ return supabase
723
+ .from('objects')
724
+ .update({
725
+ l4yercak3_id: l4yercak3Id,
726
+ sync_status: 'synced',
727
+ synced_at: new Date().toISOString(),
728
+ })
729
+ .eq('id', id);
730
+ }
731
+
732
+ module.exports = {
733
+ supabase,
734
+ createServerClient,
735
+ getObjects,
736
+ getObject,
737
+ createObject,
738
+ updateObject,
739
+ deleteObject,
740
+ getUserByEmail,
741
+ createUser,
742
+ getPendingSync,
743
+ markSynced,
744
+ };
745
+ `;
746
+ }
747
+
748
+ async generateTypes(projectPath) {
749
+ // Determine output directory
750
+ let outputDir;
751
+ if (fs.existsSync(path.join(projectPath, 'src'))) {
752
+ outputDir = path.join(projectPath, 'src', 'lib', 'supabase');
753
+ } else {
754
+ outputDir = path.join(projectPath, 'lib', 'supabase');
755
+ }
756
+
757
+ ensureDir(outputDir);
758
+
759
+ const outputPath = path.join(outputDir, 'types.ts');
760
+
761
+ const action = await checkFileOverwrite(outputPath);
762
+ if (action === 'skip') {
763
+ return null;
764
+ }
765
+
766
+ const content = `/**
767
+ * Supabase Database Types
768
+ * Auto-generated by @l4yercak3/cli
769
+ *
770
+ * Note: For full type safety, generate types from your database using:
771
+ * npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/lib/supabase/types.ts
772
+ */
773
+
774
+ export type Json =
775
+ | string
776
+ | number
777
+ | boolean
778
+ | null
779
+ | { [key: string]: Json | undefined }
780
+ | Json[];
781
+
782
+ export type SyncStatus =
783
+ | 'synced'
784
+ | 'pending_push'
785
+ | 'pending_pull'
786
+ | 'conflict'
787
+ | 'local_only';
788
+
789
+ export interface Database {
790
+ public: {
791
+ Tables: {
792
+ objects: {
793
+ Row: {
794
+ id: string;
795
+ l4yercak3_id: string | null;
796
+ organization_id: string;
797
+ type: string;
798
+ subtype: string | null;
799
+ name: string;
800
+ status: string;
801
+ custom_properties: Json;
802
+ sync_status: SyncStatus;
803
+ synced_at: string | null;
804
+ local_version: number;
805
+ remote_version: number | null;
806
+ created_at: string;
807
+ updated_at: string;
808
+ deleted_at: string | null;
809
+ };
810
+ Insert: {
811
+ id?: string;
812
+ l4yercak3_id?: string | null;
813
+ organization_id: string;
814
+ type: string;
815
+ subtype?: string | null;
816
+ name: string;
817
+ status: string;
818
+ custom_properties?: Json;
819
+ sync_status?: SyncStatus;
820
+ synced_at?: string | null;
821
+ local_version?: number;
822
+ remote_version?: number | null;
823
+ created_at?: string;
824
+ updated_at?: string;
825
+ deleted_at?: string | null;
826
+ };
827
+ Update: {
828
+ id?: string;
829
+ l4yercak3_id?: string | null;
830
+ organization_id?: string;
831
+ type?: string;
832
+ subtype?: string | null;
833
+ name?: string;
834
+ status?: string;
835
+ custom_properties?: Json;
836
+ sync_status?: SyncStatus;
837
+ synced_at?: string | null;
838
+ local_version?: number;
839
+ remote_version?: number | null;
840
+ created_at?: string;
841
+ updated_at?: string;
842
+ deleted_at?: string | null;
843
+ };
844
+ };
845
+ object_links: {
846
+ Row: {
847
+ id: string;
848
+ l4yercak3_id: string | null;
849
+ from_object_id: string;
850
+ to_object_id: string;
851
+ link_type: string;
852
+ metadata: Json | null;
853
+ sync_status: string;
854
+ created_at: string;
855
+ };
856
+ Insert: {
857
+ id?: string;
858
+ l4yercak3_id?: string | null;
859
+ from_object_id: string;
860
+ to_object_id: string;
861
+ link_type: string;
862
+ metadata?: Json | null;
863
+ sync_status?: string;
864
+ created_at?: string;
865
+ };
866
+ Update: {
867
+ id?: string;
868
+ l4yercak3_id?: string | null;
869
+ from_object_id?: string;
870
+ to_object_id?: string;
871
+ link_type?: string;
872
+ metadata?: Json | null;
873
+ sync_status?: string;
874
+ created_at?: string;
875
+ };
876
+ };
877
+ frontend_users: {
878
+ Row: {
879
+ id: string;
880
+ l4yercak3_contact_id: string | null;
881
+ l4yercak3_frontend_user_id: string | null;
882
+ organization_id: string;
883
+ email: string;
884
+ email_verified: boolean;
885
+ name: string | null;
886
+ first_name: string | null;
887
+ last_name: string | null;
888
+ image: string | null;
889
+ phone: string | null;
890
+ password_hash: string | null;
891
+ oauth_accounts: Json;
892
+ role: string;
893
+ preferences: Json;
894
+ sync_status: string;
895
+ synced_at: string | null;
896
+ created_at: string;
897
+ updated_at: string;
898
+ last_login_at: string | null;
899
+ };
900
+ Insert: {
901
+ id?: string;
902
+ l4yercak3_contact_id?: string | null;
903
+ l4yercak3_frontend_user_id?: string | null;
904
+ organization_id: string;
905
+ email: string;
906
+ email_verified?: boolean;
907
+ name?: string | null;
908
+ first_name?: string | null;
909
+ last_name?: string | null;
910
+ image?: string | null;
911
+ phone?: string | null;
912
+ password_hash?: string | null;
913
+ oauth_accounts?: Json;
914
+ role?: string;
915
+ preferences?: Json;
916
+ sync_status?: string;
917
+ synced_at?: string | null;
918
+ created_at?: string;
919
+ updated_at?: string;
920
+ last_login_at?: string | null;
921
+ };
922
+ Update: {
923
+ id?: string;
924
+ l4yercak3_contact_id?: string | null;
925
+ l4yercak3_frontend_user_id?: string | null;
926
+ organization_id?: string;
927
+ email?: string;
928
+ email_verified?: boolean;
929
+ name?: string | null;
930
+ first_name?: string | null;
931
+ last_name?: string | null;
932
+ image?: string | null;
933
+ phone?: string | null;
934
+ password_hash?: string | null;
935
+ oauth_accounts?: Json;
936
+ role?: string;
937
+ preferences?: Json;
938
+ sync_status?: string;
939
+ synced_at?: string | null;
940
+ created_at?: string;
941
+ updated_at?: string;
942
+ last_login_at?: string | null;
943
+ };
944
+ };
945
+ sessions: {
946
+ Row: {
947
+ id: string;
948
+ user_id: string;
949
+ session_token: string;
950
+ expires_at: string;
951
+ user_agent: string | null;
952
+ ip_address: string | null;
953
+ created_at: string;
954
+ };
955
+ Insert: {
956
+ id?: string;
957
+ user_id: string;
958
+ session_token: string;
959
+ expires_at: string;
960
+ user_agent?: string | null;
961
+ ip_address?: string | null;
962
+ created_at?: string;
963
+ };
964
+ Update: {
965
+ id?: string;
966
+ user_id?: string;
967
+ session_token?: string;
968
+ expires_at?: string;
969
+ user_agent?: string | null;
970
+ ip_address?: string | null;
971
+ created_at?: string;
972
+ };
973
+ };
974
+ stripe_customers: {
975
+ Row: {
976
+ id: string;
977
+ frontend_user_id: string;
978
+ stripe_customer_id: string;
979
+ l4yercak3_contact_id: string | null;
980
+ email: string;
981
+ name: string | null;
982
+ default_payment_method_id: string | null;
983
+ sync_status: string;
984
+ created_at: string;
985
+ };
986
+ Insert: {
987
+ id?: string;
988
+ frontend_user_id: string;
989
+ stripe_customer_id: string;
990
+ l4yercak3_contact_id?: string | null;
991
+ email: string;
992
+ name?: string | null;
993
+ default_payment_method_id?: string | null;
994
+ sync_status?: string;
995
+ created_at?: string;
996
+ };
997
+ Update: {
998
+ id?: string;
999
+ frontend_user_id?: string;
1000
+ stripe_customer_id?: string;
1001
+ l4yercak3_contact_id?: string | null;
1002
+ email?: string;
1003
+ name?: string | null;
1004
+ default_payment_method_id?: string | null;
1005
+ sync_status?: string;
1006
+ created_at?: string;
1007
+ };
1008
+ };
1009
+ stripe_payments: {
1010
+ Row: {
1011
+ id: string;
1012
+ stripe_payment_intent_id: string;
1013
+ stripe_customer_id: string | null;
1014
+ frontend_user_id: string | null;
1015
+ amount: number;
1016
+ currency: string;
1017
+ status: string;
1018
+ payment_method: string | null;
1019
+ metadata: Json;
1020
+ l4yercak3_order_id: string | null;
1021
+ l4yercak3_invoice_id: string | null;
1022
+ sync_status: string;
1023
+ synced_at: string | null;
1024
+ created_at: string;
1025
+ completed_at: string | null;
1026
+ };
1027
+ Insert: {
1028
+ id?: string;
1029
+ stripe_payment_intent_id: string;
1030
+ stripe_customer_id?: string | null;
1031
+ frontend_user_id?: string | null;
1032
+ amount: number;
1033
+ currency: string;
1034
+ status: string;
1035
+ payment_method?: string | null;
1036
+ metadata: Json;
1037
+ l4yercak3_order_id?: string | null;
1038
+ l4yercak3_invoice_id?: string | null;
1039
+ sync_status?: string;
1040
+ synced_at?: string | null;
1041
+ created_at?: string;
1042
+ completed_at?: string | null;
1043
+ };
1044
+ Update: {
1045
+ id?: string;
1046
+ stripe_payment_intent_id?: string;
1047
+ stripe_customer_id?: string | null;
1048
+ frontend_user_id?: string | null;
1049
+ amount?: number;
1050
+ currency?: string;
1051
+ status?: string;
1052
+ payment_method?: string | null;
1053
+ metadata?: Json;
1054
+ l4yercak3_order_id?: string | null;
1055
+ l4yercak3_invoice_id?: string | null;
1056
+ sync_status?: string;
1057
+ synced_at?: string | null;
1058
+ created_at?: string;
1059
+ completed_at?: string | null;
1060
+ };
1061
+ };
1062
+ };
1063
+ };
1064
+ }
1065
+ `;
1066
+
1067
+ return writeFileWithBackup(outputPath, content, action);
1068
+ }
1069
+
1070
+ async generateMiddleware(projectPath, options) {
1071
+ const { isTypeScript } = options;
1072
+
1073
+ const extension = isTypeScript ? 'ts' : 'js';
1074
+ const outputPath = path.join(projectPath, `middleware.${extension}`);
1075
+
1076
+ const action = await checkFileOverwrite(outputPath);
1077
+ if (action === 'skip') {
1078
+ return null;
1079
+ }
1080
+
1081
+ const content = isTypeScript
1082
+ ? `/**
1083
+ * Supabase Auth Middleware
1084
+ * Auto-generated by @l4yercak3/cli
1085
+ */
1086
+
1087
+ import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
1088
+ import { NextResponse } from 'next/server';
1089
+ import type { NextRequest } from 'next/server';
1090
+
1091
+ export async function middleware(req: NextRequest) {
1092
+ const res = NextResponse.next();
1093
+ const supabase = createMiddlewareClient({ req, res });
1094
+ await supabase.auth.getSession();
1095
+ return res;
1096
+ }
1097
+
1098
+ export const config = {
1099
+ matcher: [
1100
+ '/((?!_next/static|_next/image|favicon.ico|public).*)',
1101
+ ],
1102
+ };
1103
+ `
1104
+ : `/**
1105
+ * Supabase Auth Middleware
1106
+ * Auto-generated by @l4yercak3/cli
1107
+ */
1108
+
1109
+ const { createMiddlewareClient } = require('@supabase/auth-helpers-nextjs');
1110
+ const { NextResponse } = require('next/server');
1111
+
1112
+ async function middleware(req) {
1113
+ const res = NextResponse.next();
1114
+ const supabase = createMiddlewareClient({ req, res });
1115
+ await supabase.auth.getSession();
1116
+ return res;
1117
+ }
1118
+
1119
+ const config = {
1120
+ matcher: [
1121
+ '/((?!_next/static|_next/image|favicon.ico|public).*)',
1122
+ ],
1123
+ };
1124
+
1125
+ module.exports = { middleware, config };
1126
+ `;
1127
+
1128
+ return writeFileWithBackup(outputPath, content, action);
1129
+ }
1130
+ }
1131
+
1132
+ module.exports = new SupabaseGenerator();