suparisma 1.0.1 → 1.0.3

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 CHANGED
@@ -1,3 +1,5 @@
1
+ (BETA) - USE IN PRODUCTION AT YOUR OWN RISK.
2
+
1
3
  # Suparisma
2
4
  Supabase + Prisma!
3
5
 
@@ -790,4 +792,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
790
792
 
791
793
  ## License
792
794
 
793
- MIT
795
+ MIT
@@ -9,11 +9,8 @@ const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("../config"); // Ensure this is UTILS_DIR
10
10
  function generateSupabaseClientFile() {
11
11
  const supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
12
-
13
12
  import { createClient } from '@supabase/supabase-js';
14
13
 
15
- console.log(\`NEXT_PUBLIC_SUPABASE_URL: \${process.env.NEXT_PUBLIC_SUPABASE_URL}\`);
16
- console.log(\`NEXT_PUBLIC_SUPABASE_ANON_KEY: \${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}\`);
17
14
  export const supabase = createClient(
18
15
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
19
16
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
@@ -46,14 +46,27 @@ function generateModelTypesFile(model) {
46
46
  .filter((field) => !relationObjectFields.includes(field.name) && !foreignKeyFields.includes(field.name))
47
47
  .map((field) => {
48
48
  const isOptional = field.isOptional;
49
- const type = field.type === 'Int'
50
- ? 'number'
51
- : field.type === 'Float'
52
- ? 'number'
53
- : field.type === 'Boolean'
54
- ? 'boolean'
55
- : 'string';
56
- return ` ${field.name}${isOptional ? '?' : ''}: ${type};`;
49
+ let baseType;
50
+ switch (field.type) {
51
+ case 'Int':
52
+ case 'Float':
53
+ baseType = 'number';
54
+ break;
55
+ case 'Boolean':
56
+ baseType = 'boolean';
57
+ break;
58
+ case 'DateTime':
59
+ baseType = 'string'; // ISO date string
60
+ break;
61
+ case 'Json':
62
+ baseType = 'any'; // Or a more specific structured type if available
63
+ break;
64
+ default:
65
+ // Covers String, Enum names (e.g., "SomeEnum"), Bytes, Decimal, etc.
66
+ baseType = 'string';
67
+ }
68
+ const finalType = field.isList ? `${baseType}[]` : baseType;
69
+ return ` ${field.name}${isOptional ? '?' : ''}: ${finalType};`;
57
70
  });
58
71
  // Add foreign key fields
59
72
  foreignKeyFields.forEach((field) => {
@@ -70,14 +83,27 @@ function generateModelTypesFile(model) {
70
83
  .map((field) => {
71
84
  // Make fields with default values optional in CreateInput
72
85
  const isOptional = field.isOptional || defaultValueFields.includes(field.name);
73
- const type = field.type === 'Int'
74
- ? 'number'
75
- : field.type === 'Float'
76
- ? 'number'
77
- : field.type === 'Boolean'
78
- ? 'boolean'
79
- : 'string';
80
- return ` ${field.name}${isOptional ? '?' : ''}: ${type};`;
86
+ let baseType;
87
+ switch (field.type) {
88
+ case 'Int':
89
+ case 'Float':
90
+ baseType = 'number';
91
+ break;
92
+ case 'Boolean':
93
+ baseType = 'boolean';
94
+ break;
95
+ case 'DateTime':
96
+ baseType = 'string'; // ISO date string
97
+ break;
98
+ case 'Json':
99
+ baseType = 'any'; // Or a more specific structured type if available
100
+ break;
101
+ default:
102
+ // Covers String, Enum names (e.g., "SomeEnum"), Bytes, Decimal, etc.
103
+ baseType = 'string';
104
+ }
105
+ const finalType = field.isList ? `${baseType}[]` : baseType;
106
+ return ` ${field.name}${isOptional ? '?' : ''}: ${finalType};`;
81
107
  });
82
108
  // Add foreign key fields to CreateInput
83
109
  foreignKeyFields.forEach((field) => {
package/dist/index.js CHANGED
@@ -90,32 +90,41 @@ function analyzePrismaSchema(schemaPath) {
90
90
  try {
91
91
  const schemaContent = fs_1.default.readFileSync(schemaPath, 'utf8');
92
92
  const modelInfos = [];
93
- // Regular expression to match model definitions with comments
94
93
  const modelRegex = /(?:\/\/\s*@disableRealtime\s*)?\s*model\s+(\w+)\s*{([\s\S]*?)}/g;
95
94
  let modelMatch;
96
95
  while ((modelMatch = modelRegex.exec(schemaContent)) !== null) {
97
96
  const modelName = modelMatch[1];
98
- const modelBody = modelMatch[2];
97
+ const modelBodyWithComments = modelMatch[0]; // Includes the model keyword and its comments
98
+ const modelBody = modelMatch[2]; // Just the content within {}
99
99
  if (!modelName || !modelBody) {
100
100
  console.error('Model name or body not found');
101
101
  continue;
102
102
  }
103
- const tableName = modelMatch[0].includes('@map')
104
- ? modelMatch[0].match(/@map\s*\(\s*["'](.+?)["']\s*\)/)?.at(1) || modelName
103
+ const tableName = modelBodyWithComments.includes('@map')
104
+ ? modelBodyWithComments.match(/@map\s*\(\s*["'](.+?)["']\s*\)/)?.at(1) || modelName
105
105
  : modelName;
106
- // Check if model has @disableRealtime comment
107
- // Default is to enable realtime unless explicitly disabled
108
- const enableRealtime = !modelMatch[0].includes('// @disableRealtime');
109
- // Find fields with @enableSearch comment
106
+ const enableRealtime = !modelBodyWithComments.includes('// @disableRealtime');
110
107
  const searchFields = [];
111
- const fieldRegex = /(\w+)\s+(\w+)(?:\?.+?)?\s+(?:@.+?)?\s*(?:\/\/\s*@enableSearch)?/g;
112
- let fieldMatch;
113
- while ((fieldMatch = fieldRegex.exec(modelBody)) !== null) {
114
- if (fieldMatch[0].includes('// @enableSearch')) {
115
- searchFields.push({
116
- name: fieldMatch[1] || '',
117
- type: fieldMatch[2] || '',
118
- });
108
+ // Split model body into lines to check preceding line for @enableSearch
109
+ const bodyLines = modelBody.trim().split('\n');
110
+ for (let i = 0; i < bodyLines.length; i++) {
111
+ const currentLine = bodyLines[i].trim();
112
+ const previousLine = i > 0 ? bodyLines[i - 1].trim() : "";
113
+ // Check if the PREVIOUS line contains the @enableSearch comment
114
+ if (previousLine.includes('// @enableSearch')) {
115
+ // Try to parse the CURRENT line as a field definition
116
+ // Basic regex: fieldName fieldType (optional attributes/comments)
117
+ const fieldMatch = currentLine.match(/^(\w+)\s+(\w+)/);
118
+ if (fieldMatch) {
119
+ const fieldName = fieldMatch[1];
120
+ const fieldType = fieldMatch[2];
121
+ if (fieldName && fieldType) {
122
+ searchFields.push({
123
+ name: fieldName,
124
+ type: fieldType,
125
+ });
126
+ }
127
+ }
119
128
  }
120
129
  }
121
130
  modelInfos.push({
@@ -150,123 +159,109 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
150
159
  }
151
160
  // Analyze Prisma schema for models, realtime and search annotations
152
161
  const modelInfos = analyzePrismaSchema(schemaPath);
153
- // Dynamically import pg package
154
162
  const pg = await Promise.resolve().then(() => __importStar(require('pg')));
155
163
  const { Pool } = pg.default || pg;
156
- // Connect to PostgreSQL database
157
- const pool = new Pool({ connectionString: directUrl });
158
- console.log('🔌 Connected to PostgreSQL database');
159
- // Get all tables from database directly
160
- const { rows: allTables } = await pool.query(`
161
- SELECT table_name
162
- FROM information_schema.tables
163
- WHERE table_schema = 'public'
164
- `);
165
- console.log(`📋 Found ${allTables.length} tables in the 'public' schema`);
166
- allTables.forEach((t) => console.log(` - ${t.table_name}`));
167
- // DIRECT APPROACH: Hardcode SQL for each known Prisma model type
164
+ const pool = new Pool({ connectionString: process.env.DIRECT_URL });
165
+ console.log('🔌 Connected to PostgreSQL database for configuration.');
166
+ const { rows: allTables } = await pool.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`);
168
167
  for (const model of modelInfos) {
169
- try {
170
- // Find the matching table regardless of case
171
- const matchingTable = allTables.find((t) => t.table_name.toLowerCase() === model.tableName.toLowerCase());
172
- if (!matchingTable) {
173
- console.warn(`âš ī¸ Could not find a table for model ${model.name}. Skipping.`);
174
- continue;
168
+ const matchingTable = allTables.find((t) => t.table_name.toLowerCase() === model.tableName.toLowerCase());
169
+ if (!matchingTable) {
170
+ console.warn(`🟠 Skipping model ${model.name}: Corresponding table ${model.tableName} not found in database.`);
171
+ continue;
172
+ }
173
+ const actualTableName = matchingTable.table_name;
174
+ console.log(`Processing model ${model.name} (table: "${actualTableName}")`);
175
+ // Realtime setup (existing logic)
176
+ if (model.enableRealtime) {
177
+ const alterPublicationQuery = `ALTER PUBLICATION supabase_realtime ADD TABLE "${actualTableName}";`;
178
+ try {
179
+ await pool.query(alterPublicationQuery);
180
+ console.log(` ✅ Added "${actualTableName}" to supabase_realtime publication for real-time updates.`);
175
181
  }
176
- // Use the exact case of the table as it exists in the database
177
- const actualTableName = matchingTable.table_name;
178
- console.log(`🔍 Model ${model.name} -> Actual table: ${actualTableName}`);
179
- console.log(`â„šī¸ Model ${model.name}: enableRealtime is ${model.enableRealtime}`);
180
- if (model.enableRealtime) {
181
- // Explicitly use double quotes for mixed case identifiers
182
- // try {
183
- // await pool.query(`ALTER TABLE "${actualTableName}" REPLICA IDENTITY FULL;`);
184
- // console.log(`✅ Set REPLICA IDENTITY FULL on "${actualTableName}"`);
185
- // } catch (err: any ) {
186
- // console.error(`❌ Failed to set REPLICA IDENTITY on "${actualTableName}": ${err.message}`);
187
- // }
188
- // Directly add the table to Supabase Realtime publication
189
- const alterPublicationQuery = `ALTER PUBLICATION supabase_realtime ADD TABLE "${actualTableName}";`;
190
- console.log(`â„šī¸ Executing SQL: ${alterPublicationQuery}`);
191
- try {
192
- await pool.query(alterPublicationQuery);
193
- console.log(`✅ Added "${actualTableName}" to supabase_realtime publication`);
182
+ catch (err) {
183
+ if (err.message.includes('already member')) {
184
+ console.log(` â„šī¸ Table "${actualTableName}" was already in supabase_realtime publication.`);
194
185
  }
195
- catch (err) {
196
- // If error contains "already exists", this is fine
197
- if (err.message.includes('already member')) {
198
- console.log(`â„šī¸ Table "${actualTableName}" was already in supabase_realtime publication`);
199
- }
200
- else {
201
- console.error(`❌ Failed to add "${actualTableName}" to supabase_realtime. Full error:`, err);
202
- }
186
+ else {
187
+ console.error(` ❌ Failed to add "${actualTableName}" to supabase_realtime: ${err.message}`);
203
188
  }
204
189
  }
205
- // Handle search fields if any
206
- if (model.searchFields.length > 0) {
207
- // Get all columns for this table
208
- const { rows: columns } = await pool.query(`
209
- SELECT column_name
210
- FROM information_schema.columns
211
- WHERE table_schema = 'public' AND table_name = $1
212
- `, [actualTableName]);
213
- for (const searchField of model.searchFields) {
214
- // Find matching column regardless of case
215
- const matchingColumn = columns.find((c) => c.column_name.toLowerCase() === searchField.name.toLowerCase());
216
- if (!matchingColumn) {
217
- console.warn(`âš ī¸ Could not find column ${searchField.name} in table ${actualTableName}. Skipping search function.`);
218
- continue;
219
- }
220
- const actualColumnName = matchingColumn.column_name;
221
- const functionName = `search_${actualTableName.toLowerCase()}_by_${actualColumnName.toLowerCase()}_prefix`;
222
- const indexName = `idx_search_${actualTableName.toLowerCase()}_${actualColumnName.toLowerCase()}`;
223
- try {
224
- // Create search function with exact column case
225
- await pool.query(`
226
- CREATE OR REPLACE FUNCTION "public"."${functionName}"(prefix text)
227
- RETURNS SETOF "public"."${actualTableName}" AS $$
228
- BEGIN
229
- RETURN QUERY
230
- SELECT * FROM "public"."${actualTableName}"
231
- WHERE to_tsvector('english', "${actualColumnName}") @@ to_tsquery('english', prefix || ':*');
232
- END;
233
- $$ LANGUAGE plpgsql;
234
- `);
235
- console.log(`✅ Created search function for ${actualTableName}.${actualColumnName}`);
236
- // FIXED: Properly quote identifiers in the index creation query
237
- await pool.query(`
238
- DO $$
239
- BEGIN
240
- IF NOT EXISTS (
241
- SELECT 1 FROM pg_indexes
242
- WHERE schemaname = 'public'
243
- AND tablename = '${actualTableName}'
244
- AND indexname = '${indexName}'
245
- ) THEN
246
- CREATE INDEX "${indexName}" ON "public"."${actualTableName}"
247
- USING GIN (to_tsvector('english', "${actualColumnName}"));
248
- END IF;
249
- END;
250
- $$;
251
- `);
252
- console.log(`✅ Created search index for ${actualTableName}.${actualColumnName}`);
190
+ }
191
+ else {
192
+ console.log(` â„šī¸ Realtime disabled for model ${model.name}.`);
193
+ }
194
+ // Search setup
195
+ if (model.searchFields.length > 0) {
196
+ console.log(` 🔍 Setting up full-text search for model ${model.name}:`);
197
+ const { rows: columns } = await pool.query(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [actualTableName]);
198
+ for (const searchField of model.searchFields) {
199
+ const matchingColumn = columns.find((c) => c.column_name.toLowerCase() === searchField.name.toLowerCase());
200
+ if (!matchingColumn) {
201
+ console.warn(` 🟠 Skipping search for field ${searchField.name}: Column not found in table "${actualTableName}".`);
202
+ continue;
203
+ }
204
+ const actualColumnName = matchingColumn.column_name;
205
+ const functionName = `search_${actualTableName.toLowerCase()}_by_${actualColumnName.toLowerCase()}_prefix`;
206
+ const indexName = `idx_gin_search_${actualTableName.toLowerCase()}_${actualColumnName.toLowerCase()}`;
207
+ console.log(` âžĄī¸ Configuring field "${actualColumnName}":`);
208
+ try {
209
+ // Create search function
210
+ const createFunctionQuery = `
211
+ CREATE OR REPLACE FUNCTION "public"."${functionName}"(search_prefix text)
212
+ RETURNS SETOF "public"."${actualTableName}" AS $$
213
+ BEGIN
214
+ RETURN QUERY
215
+ SELECT * FROM "public"."${actualTableName}"
216
+ WHERE to_tsvector('english', "${actualColumnName}") @@ to_tsquery('english', search_prefix || ':*');
217
+ END;
218
+ $$ LANGUAGE plpgsql STABLE;`; // Added STABLE for potential performance benefits
219
+ await pool.query(createFunctionQuery);
220
+ console.log(` ✅ Created/Replaced RPC function: "${functionName}"(search_prefix text)`);
221
+ // Create GIN index
222
+ const createIndexQuery = `
223
+ DO $$
224
+ BEGIN
225
+ IF NOT EXISTS (
226
+ SELECT 1 FROM pg_indexes
227
+ WHERE schemaname = 'public'
228
+ AND tablename = '${actualTableName}'
229
+ AND indexname = '${indexName}'
230
+ ) THEN
231
+ CREATE INDEX "${indexName}" ON "public"."${actualTableName}" USING GIN (to_tsvector('english', "${actualColumnName}"));
232
+ RAISE NOTICE ' ✅ Created GIN index: "${indexName}" on "${actualTableName}"("${actualColumnName}")';
233
+ ELSE
234
+ RAISE NOTICE ' â„šī¸ GIN index "${indexName}" on "${actualTableName}"("${actualColumnName}") already exists.';
235
+ END IF;
236
+ END;
237
+ $$;`;
238
+ const indexResult = await pool.query(createIndexQuery);
239
+ // Output notices from the DO $$ block (PostgreSQL specific)
240
+ if (indexResult.rows.length > 0 && indexResult.rows[0].notice) {
241
+ console.log(indexResult.rows[0].notice.replace(/^NOTICE: /, ''));
253
242
  }
254
- catch (err) {
255
- console.error(`❌ Failed to set up search for "${actualTableName}.${actualColumnName}": ${err.message}`);
243
+ else if (!indexResult.rows.find((r) => r.notice?.includes('Created GIN index'))) {
244
+ // If DO $$ block doesn't emit specific notice for creation and it didn't say exists.
245
+ // This is a fallback log, actual creation/existence is handled by the DO block.
246
+ // The important part is that the index will be there.
256
247
  }
257
248
  }
249
+ catch (err) {
250
+ console.error(` ❌ Failed to set up search for "${actualTableName}"."${actualColumnName}": ${err.message}`);
251
+ }
258
252
  }
259
253
  }
260
- catch (err) {
261
- console.error(`❌ Error processing model ${model.name}: ${err.message}`);
254
+ else {
255
+ console.log(` â„šī¸ No fields marked with // @enableSearch for model ${model.name}.`);
262
256
  }
257
+ console.log('---------------------------------------------------');
263
258
  }
264
259
  await pool.end();
265
- console.log('🎉 Database configuration complete');
260
+ console.log('🎉 Database configuration complete.');
266
261
  }
267
262
  catch (err) {
268
- console.error('❌ Error configuring database:', err);
269
- console.log('âš ī¸ Continuing with hook generation anyway...');
263
+ console.error('❌ Error during database configuration:', err);
264
+ console.log('âš ī¸ Hook generation will continue, but database features like search or realtime might not be fully configured.');
270
265
  }
271
266
  }
272
267
  /**
package/dist/parser.js CHANGED
@@ -44,15 +44,16 @@ function parsePrismaSchema(schemaPath) {
44
44
  if (line.startsWith('//')) {
45
45
  continue;
46
46
  }
47
- // Parse field definition
48
- const fieldMatch = line.match(/\s*(\w+)\s+(\w+)(\?)?\s*(?:@[^)]+)?/);
47
+ // Parse field definition - Updated to handle array types
48
+ const fieldMatch = line.match(/\s*(\w+)\s+(\w+)(\[\])?\??(\?)?\s*(?:@[^)]+)?/);
49
49
  if (fieldMatch) {
50
50
  const fieldName = fieldMatch[1];
51
- const fieldType = fieldMatch[2];
52
- const isOptional = !!fieldMatch[3]; // ? makes it optional
51
+ const baseFieldType = fieldMatch[2]; // e.g., "String" from "String[]"
52
+ const isArray = !!fieldMatch[3]; // [] makes it an array
53
+ const isOptional = !!fieldMatch[4]; // ? makes it optional
53
54
  // Store for potential standalone @enableSearch comment
54
55
  lastFieldName = fieldName || '';
55
- lastFieldType = fieldType || '';
56
+ lastFieldType = baseFieldType || '';
56
57
  // Detect special fields
57
58
  const isId = line.includes('@id');
58
59
  const isCreatedAt = fieldName === 'created_at' || fieldName === 'createdAt';
@@ -73,13 +74,13 @@ function parsePrismaSchema(schemaPath) {
73
74
  if (line.includes('// @enableSearch')) {
74
75
  searchFields.push({
75
76
  name: fieldName || '',
76
- type: fieldType || '',
77
+ type: baseFieldType || '',
77
78
  });
78
79
  }
79
- if (fieldName && fieldType) {
80
+ if (fieldName && baseFieldType) {
80
81
  fields.push({
81
82
  name: fieldName,
82
- type: fieldType,
83
+ type: baseFieldType, // Store the base type (String, not String[])
83
84
  isRequired: false,
84
85
  isOptional,
85
86
  isId,
@@ -89,6 +90,7 @@ function parsePrismaSchema(schemaPath) {
89
90
  hasDefaultValue,
90
91
  defaultValue, // Add the extracted default value
91
92
  isRelation,
93
+ isList: isArray, // Add the isList property
92
94
  });
93
95
  }
94
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma.",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -19,10 +19,12 @@ enum SomeEnum {
19
19
  THREE
20
20
  }
21
21
 
22
- // Realtime is enabled by default for all models
22
+
23
23
  model Thing {
24
24
  id String @id @default(uuid())
25
- name String?
25
+ // @enableSearch
26
+ name String?
27
+ stringArray String[]
26
28
  someEnum SomeEnum @default(ONE)
27
29
  someNumber Int?
28
30
  createdAt DateTime @default(now())