suparisma 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/config.js +7 -0
- package/dist/generated/supabase-client-generated.js +7 -0
- package/dist/generators/coreGenerator.js +1430 -0
- package/dist/generators/hookGenerator.js +108 -0
- package/dist/generators/indexGenerator.js +100 -0
- package/dist/generators/supabaseClientGenerator.js +29 -0
- package/dist/generators/typeGenerator.js +566 -0
- package/dist/hooks/generated/UserTypes.js +2 -0
- package/dist/hooks/generated/core.js +1089 -0
- package/dist/hooks/generated/index.js +33 -0
- package/dist/hooks/generated/useSuparismaUser.js +60 -0
- package/dist/index.js +259 -0
- package/dist/parser.js +117 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
- package/prisma/schema.prisma +22 -0
- package/src/config.ts +7 -0
- package/src/generated/hooks/useSuparismaUser.ts +77 -0
- package/src/generated/index.ts +50 -0
- package/src/generated/types/UserTypes.ts +400 -0
- package/src/generated/utils/core.ts +1413 -0
- package/src/generated/utils/supabase-client.ts +7 -0
- package/src/generators/coreGenerator.ts +1426 -0
- package/src/generators/hookGenerator.ts +110 -0
- package/src/generators/indexGenerator.ts +117 -0
- package/src/generators/supabaseClientGenerator.ts +24 -0
- package/src/generators/typeGenerator.ts +587 -0
- package/src/index.ts +339 -0
- package/src/parser.ts +134 -0
- package/src/suparisma/generated/UserTypes.ts +400 -0
- package/src/suparisma/generated/core.ts +1413 -0
- package/src/suparisma/generated/hooks/useSuparismaUser.ts +77 -0
- package/src/suparisma/generated/index.ts +50 -0
- package/src/suparisma/generated/supabase-client-generated.ts +9 -0
- package/src/suparisma/generated/types/UserTypes.ts +400 -0
- package/src/suparisma/generated/useSuparismaUser.ts +77 -0
- package/src/suparisma/generated/utils/core.ts +1413 -0
- package/src/suparisma/generated/utils/supabase-client.ts +7 -0
- package/src/types.ts +57 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
3
|
+
// Edit the generator script instead: scripts/generate-realtime-hooks.ts
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
const useSuparismaUser_1 = require("./useSuparismaUser");
|
|
6
|
+
/**
|
|
7
|
+
* Main Suparisma hook object with dot notation access to all model hooks.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Get hooks for different models
|
|
11
|
+
* import useSuparisma from './hooks/generated';
|
|
12
|
+
*
|
|
13
|
+
* // Access user model with all hook methods
|
|
14
|
+
* const users = useSuparisma.user();
|
|
15
|
+
* const { data, loading, error } = users;
|
|
16
|
+
*
|
|
17
|
+
* // Create a new record
|
|
18
|
+
* await users.create({ name: "John" });
|
|
19
|
+
*
|
|
20
|
+
* // Delete a record
|
|
21
|
+
* await users.delete({ id: "123" });
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Use with filtering and options
|
|
25
|
+
* const admins = useSuparisma.user({
|
|
26
|
+
* where: { role: 'admin' },
|
|
27
|
+
* orderBy: { created_at: 'desc' }
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
const useSuparisma = {
|
|
31
|
+
user: useSuparismaUser_1.useSuparismaUser,
|
|
32
|
+
};
|
|
33
|
+
exports.default = useSuparisma;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
3
|
+
// Edit the generator script instead: scripts/generate-realtime-hooks.ts
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.useSuparismaUser = void 0;
|
|
6
|
+
const core_1 = require("./core");
|
|
7
|
+
/**
|
|
8
|
+
* A Prisma-like hook for interacting with User records with real-time capabilities.
|
|
9
|
+
*
|
|
10
|
+
* This hook provides CRUD operations, real-time updates, and search functionality.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Optional configuration options for the hook
|
|
13
|
+
* @returns An object with data state and methods for interacting with User records
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Basic usage - get all User records with realtime updates
|
|
17
|
+
* const user = useSuparismaUser();
|
|
18
|
+
* const { data, loading, error } = user;
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // With filtering and ordering
|
|
22
|
+
* const user = useSuparismaUser({
|
|
23
|
+
* where: { active: true },
|
|
24
|
+
* orderBy: { created_at: 'desc' },
|
|
25
|
+
* limit: 10
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Create a new record
|
|
30
|
+
* const result = await user.create({
|
|
31
|
+
* name: "Example Name",
|
|
32
|
+
* // other fields...
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Update a record
|
|
37
|
+
* const result = await user.update({
|
|
38
|
+
* where: { id: "123" },
|
|
39
|
+
* data: { name: "Updated Name" }
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Delete a record
|
|
44
|
+
* const result = await user.delete({ id: "123" });
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Find records with specific criteria
|
|
48
|
+
* const result = await user.findMany({
|
|
49
|
+
* where: { // filters },
|
|
50
|
+
* orderBy: { // ordering },
|
|
51
|
+
* take: 20 // limit
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
exports.useSuparismaUser = (0, core_1.createSuparismaHook)({
|
|
55
|
+
tableName: 'User',
|
|
56
|
+
hasCreatedAt: true,
|
|
57
|
+
hasUpdatedAt: true,
|
|
58
|
+
// Default values from schema
|
|
59
|
+
defaultValues: { "id": "uuid(", "createdAt": "now(" }
|
|
60
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
const fs_1 = __importDefault(require("fs"));
|
|
31
|
+
const dotenv = __importStar(require("dotenv"));
|
|
32
|
+
dotenv.config();
|
|
33
|
+
// Import configuration
|
|
34
|
+
const config_1 = require("./config");
|
|
35
|
+
// Import parsers and generators
|
|
36
|
+
const parser_1 = require("./parser");
|
|
37
|
+
const coreGenerator_1 = require("./generators/coreGenerator");
|
|
38
|
+
const typeGenerator_1 = require("./generators/typeGenerator");
|
|
39
|
+
const hookGenerator_1 = require("./generators/hookGenerator");
|
|
40
|
+
const indexGenerator_1 = require("./generators/indexGenerator");
|
|
41
|
+
const supabaseClientGenerator_1 = require("./generators/supabaseClientGenerator");
|
|
42
|
+
function analyzePrismaSchema(schemaPath) {
|
|
43
|
+
try {
|
|
44
|
+
const schemaContent = fs_1.default.readFileSync(schemaPath, 'utf8');
|
|
45
|
+
const modelInfos = [];
|
|
46
|
+
// Regular expression to match model definitions with comments
|
|
47
|
+
const modelRegex = /(?:\/\/\s*@enableRealtime\s*)?\s*model\s+(\w+)\s*{([\s\S]*?)}/g;
|
|
48
|
+
let modelMatch;
|
|
49
|
+
while ((modelMatch = modelRegex.exec(schemaContent)) !== null) {
|
|
50
|
+
const modelName = modelMatch[1];
|
|
51
|
+
const modelBody = modelMatch[2];
|
|
52
|
+
if (!modelName || !modelBody) {
|
|
53
|
+
console.error('Model name or body not found');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const tableName = modelMatch[0].includes('@map')
|
|
57
|
+
? modelMatch[0].match(/@map\s*\(\s*["'](.+?)["']\s*\)/)?.at(1) || modelName
|
|
58
|
+
: modelName;
|
|
59
|
+
// Check if model has @enableRealtime comment
|
|
60
|
+
const enableRealtime = modelMatch[0].includes('// @enableRealtime');
|
|
61
|
+
// Find fields with @enableSearch comment
|
|
62
|
+
const searchFields = [];
|
|
63
|
+
const fieldRegex = /(\w+)\s+(\w+)(?:\?.+?)?\s+(?:@.+?)?\s*(?:\/\/\s*@enableSearch)?/g;
|
|
64
|
+
let fieldMatch;
|
|
65
|
+
while ((fieldMatch = fieldRegex.exec(modelBody)) !== null) {
|
|
66
|
+
if (fieldMatch[0].includes('// @enableSearch')) {
|
|
67
|
+
searchFields.push({
|
|
68
|
+
name: fieldMatch[1] || '',
|
|
69
|
+
type: fieldMatch[2] || '',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
modelInfos.push({
|
|
74
|
+
name: modelName,
|
|
75
|
+
tableName,
|
|
76
|
+
enableRealtime,
|
|
77
|
+
searchFields,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return modelInfos;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('Error analyzing Prisma schema:', error);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Configure database tables for proper realtime functionality and search
|
|
89
|
+
* 1. Sets REPLICA IDENTITY FULL and enables realtime for models with @enableRealtime
|
|
90
|
+
* 2. Creates search functions for fields with @enableSearch
|
|
91
|
+
*/
|
|
92
|
+
async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
93
|
+
try {
|
|
94
|
+
// COMPLETELY BYPASS NORMAL OPERATION FOR SIMPLICITY
|
|
95
|
+
console.log('🔧 Using direct SQL approach to avoid PostgreSQL case sensitivity issues...');
|
|
96
|
+
// Load environment variables
|
|
97
|
+
dotenv.config();
|
|
98
|
+
// Get direct PostgreSQL connection URL
|
|
99
|
+
const directUrl = process.env.DIRECT_URL;
|
|
100
|
+
if (!directUrl) {
|
|
101
|
+
console.warn('⚠️ DIRECT_URL environment variable not found. Skipping database configuration.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Analyze Prisma schema for models, realtime and search annotations
|
|
105
|
+
const modelInfos = analyzePrismaSchema(schemaPath);
|
|
106
|
+
// Dynamically import pg package
|
|
107
|
+
const pg = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
108
|
+
const { Pool } = pg.default || pg;
|
|
109
|
+
// Connect to PostgreSQL database
|
|
110
|
+
const pool = new Pool({ connectionString: directUrl });
|
|
111
|
+
console.log('🔌 Connected to PostgreSQL database');
|
|
112
|
+
// Get all tables from database directly
|
|
113
|
+
const { rows: allTables } = await pool.query(`
|
|
114
|
+
SELECT table_name
|
|
115
|
+
FROM information_schema.tables
|
|
116
|
+
WHERE table_schema = 'public'
|
|
117
|
+
`);
|
|
118
|
+
console.log(`📋 Found ${allTables.length} tables in the 'public' schema`);
|
|
119
|
+
allTables.forEach((t) => console.log(` - ${t.table_name}`));
|
|
120
|
+
// DIRECT APPROACH: Hardcode SQL for each known Prisma model type
|
|
121
|
+
for (const model of modelInfos) {
|
|
122
|
+
try {
|
|
123
|
+
// Find the matching table regardless of case
|
|
124
|
+
const matchingTable = allTables.find((t) => t.table_name.toLowerCase() === model.tableName.toLowerCase());
|
|
125
|
+
if (!matchingTable) {
|
|
126
|
+
console.warn(`⚠️ Could not find a table for model ${model.name}. Skipping.`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Use the exact case of the table as it exists in the database
|
|
130
|
+
const actualTableName = matchingTable.table_name;
|
|
131
|
+
console.log(`🔍 Model ${model.name} -> Actual table: ${actualTableName}`);
|
|
132
|
+
if (model.enableRealtime) {
|
|
133
|
+
// Explicitly use double quotes for mixed case identifiers
|
|
134
|
+
// try {
|
|
135
|
+
// await pool.query(`ALTER TABLE "${actualTableName}" REPLICA IDENTITY FULL;`);
|
|
136
|
+
// console.log(`✅ Set REPLICA IDENTITY FULL on "${actualTableName}"`);
|
|
137
|
+
// } catch (err: any ) {
|
|
138
|
+
// console.error(`❌ Failed to set REPLICA IDENTITY on "${actualTableName}": ${err.message}`);
|
|
139
|
+
// }
|
|
140
|
+
// Directly add the table to Supabase Realtime publication
|
|
141
|
+
try {
|
|
142
|
+
await pool.query(`
|
|
143
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE "${actualTableName}";
|
|
144
|
+
`);
|
|
145
|
+
console.log(`✅ Added "${actualTableName}" to supabase_realtime publication`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
// If error contains "already exists", this is fine
|
|
149
|
+
if (err.message.includes('already member')) {
|
|
150
|
+
console.log(`ℹ️ Table "${actualTableName}" was already in supabase_realtime publication`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.error(`❌ Failed to add "${actualTableName}" to supabase_realtime: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Handle search fields if any
|
|
158
|
+
if (model.searchFields.length > 0) {
|
|
159
|
+
// Get all columns for this table
|
|
160
|
+
const { rows: columns } = await pool.query(`
|
|
161
|
+
SELECT column_name
|
|
162
|
+
FROM information_schema.columns
|
|
163
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
164
|
+
`, [actualTableName]);
|
|
165
|
+
for (const searchField of model.searchFields) {
|
|
166
|
+
// Find matching column regardless of case
|
|
167
|
+
const matchingColumn = columns.find((c) => c.column_name.toLowerCase() === searchField.name.toLowerCase());
|
|
168
|
+
if (!matchingColumn) {
|
|
169
|
+
console.warn(`⚠️ Could not find column ${searchField.name} in table ${actualTableName}. Skipping search function.`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const actualColumnName = matchingColumn.column_name;
|
|
173
|
+
const functionName = `search_${actualTableName.toLowerCase()}_by_${actualColumnName.toLowerCase()}_prefix`;
|
|
174
|
+
const indexName = `idx_search_${actualTableName.toLowerCase()}_${actualColumnName.toLowerCase()}`;
|
|
175
|
+
try {
|
|
176
|
+
// Create search function with exact column case
|
|
177
|
+
await pool.query(`
|
|
178
|
+
CREATE OR REPLACE FUNCTION "public"."${functionName}"(prefix text)
|
|
179
|
+
RETURNS SETOF "public"."${actualTableName}" AS $$
|
|
180
|
+
BEGIN
|
|
181
|
+
RETURN QUERY
|
|
182
|
+
SELECT * FROM "public"."${actualTableName}"
|
|
183
|
+
WHERE to_tsvector('english', "${actualColumnName}") @@ to_tsquery('english', prefix || ':*');
|
|
184
|
+
END;
|
|
185
|
+
$$ LANGUAGE plpgsql;
|
|
186
|
+
`);
|
|
187
|
+
console.log(`✅ Created search function for ${actualTableName}.${actualColumnName}`);
|
|
188
|
+
// FIXED: Properly quote identifiers in the index creation query
|
|
189
|
+
await pool.query(`
|
|
190
|
+
DO $$
|
|
191
|
+
BEGIN
|
|
192
|
+
IF NOT EXISTS (
|
|
193
|
+
SELECT 1 FROM pg_indexes
|
|
194
|
+
WHERE schemaname = 'public'
|
|
195
|
+
AND tablename = '${actualTableName}'
|
|
196
|
+
AND indexname = '${indexName}'
|
|
197
|
+
) THEN
|
|
198
|
+
CREATE INDEX "${indexName}" ON "public"."${actualTableName}"
|
|
199
|
+
USING GIN (to_tsvector('english', "${actualColumnName}"));
|
|
200
|
+
END IF;
|
|
201
|
+
END;
|
|
202
|
+
$$;
|
|
203
|
+
`);
|
|
204
|
+
console.log(`✅ Created search index for ${actualTableName}.${actualColumnName}`);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
console.error(`❌ Failed to set up search for "${actualTableName}.${actualColumnName}": ${err.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.error(`❌ Error processing model ${model.name}: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await pool.end();
|
|
217
|
+
console.log('🎉 Database configuration complete');
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
console.error('❌ Error configuring database:', err);
|
|
221
|
+
console.log('⚠️ Continuing with hook generation anyway...');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Main execution function
|
|
226
|
+
*/
|
|
227
|
+
async function main() {
|
|
228
|
+
try {
|
|
229
|
+
console.log('Generating Supabase realtime hooks with improved architecture...');
|
|
230
|
+
// Ensure output directory exists
|
|
231
|
+
if (!fs_1.default.existsSync(config_1.OUTPUT_DIR)) {
|
|
232
|
+
fs_1.default.mkdirSync(config_1.OUTPUT_DIR, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
// Generate Supabase client file first
|
|
235
|
+
(0, supabaseClientGenerator_1.generateSupabaseClientFile)();
|
|
236
|
+
// First, generate the core hook factory
|
|
237
|
+
(0, coreGenerator_1.generateCoreFile)();
|
|
238
|
+
// Parse models from Prisma schema
|
|
239
|
+
const models = (0, parser_1.parsePrismaSchema)(config_1.PRISMA_SCHEMA_PATH);
|
|
240
|
+
// Configure database tables for real-time and search functionality
|
|
241
|
+
await configurePrismaTablesForSuparisma(config_1.PRISMA_SCHEMA_PATH);
|
|
242
|
+
// Generate type definitions and hooks for each model
|
|
243
|
+
const modelInfos = [];
|
|
244
|
+
for (const model of models) {
|
|
245
|
+
const modelInfo = (0, typeGenerator_1.generateModelTypesFile)(model);
|
|
246
|
+
(0, hookGenerator_1.generateModelHookFile)(modelInfo);
|
|
247
|
+
modelInfos.push(modelInfo);
|
|
248
|
+
}
|
|
249
|
+
// Generate the main module file
|
|
250
|
+
(0, indexGenerator_1.generateMainIndexFile)(modelInfos);
|
|
251
|
+
console.log('✅ Successfully generated all suparisma hooks!');
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error('❌ Error generating hooks:', error);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Execute main function
|
|
259
|
+
main();
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parsePrismaSchema = parsePrismaSchema;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
/**
|
|
9
|
+
* Parse Prisma schema to extract model information including search annotations
|
|
10
|
+
*/
|
|
11
|
+
function parsePrismaSchema(schemaPath) {
|
|
12
|
+
const schema = fs_1.default.readFileSync(schemaPath, 'utf-8');
|
|
13
|
+
const modelRegex = /model\s+(\w+)\s+{([^}]*)}/gs;
|
|
14
|
+
const models = [];
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = modelRegex.exec(schema)) !== null) {
|
|
17
|
+
const modelName = match[1] || '';
|
|
18
|
+
const modelBody = match[2] || '';
|
|
19
|
+
// Extract custom table name if provided with @@map
|
|
20
|
+
const mapMatch = modelBody.match(/@@map\("([^"]+)"\)/);
|
|
21
|
+
const mappedName = mapMatch ? mapMatch[1] : modelName;
|
|
22
|
+
// Extract field info
|
|
23
|
+
const fields = [];
|
|
24
|
+
// Track fields with @enableSearch annotation
|
|
25
|
+
const searchFields = [];
|
|
26
|
+
const lines = modelBody.split('\n');
|
|
27
|
+
let lastFieldName = '';
|
|
28
|
+
let lastFieldType = '';
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i]?.trim();
|
|
31
|
+
// Skip blank lines and non-field lines
|
|
32
|
+
if (!line || line.startsWith('@@')) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Check for standalone @enableSearch comment
|
|
36
|
+
if (line === '// @enableSearch' && lastFieldName) {
|
|
37
|
+
searchFields.push({
|
|
38
|
+
name: lastFieldName,
|
|
39
|
+
type: lastFieldType,
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Check if line is a comment
|
|
44
|
+
if (line.startsWith('//')) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Parse field definition
|
|
48
|
+
const fieldMatch = line.match(/\s*(\w+)\s+(\w+)(\?)?\s*(?:@[^)]+)?/);
|
|
49
|
+
if (fieldMatch) {
|
|
50
|
+
const fieldName = fieldMatch[1];
|
|
51
|
+
const fieldType = fieldMatch[2];
|
|
52
|
+
const isOptional = !!fieldMatch[3]; // ? makes it optional
|
|
53
|
+
// Store for potential standalone @enableSearch comment
|
|
54
|
+
lastFieldName = fieldName || '';
|
|
55
|
+
lastFieldType = fieldType || '';
|
|
56
|
+
// Detect special fields
|
|
57
|
+
const isId = line.includes('@id');
|
|
58
|
+
const isCreatedAt = fieldName === 'created_at' || fieldName === 'createdAt';
|
|
59
|
+
const isUpdatedAt = fieldName === 'updated_at' || fieldName === 'updatedAt';
|
|
60
|
+
const hasDefaultValue = line.includes('@default');
|
|
61
|
+
// Extract default value if present
|
|
62
|
+
let defaultValue;
|
|
63
|
+
if (hasDefaultValue) {
|
|
64
|
+
const defaultMatch = line.match(/@default\(\s*(.+?)\s*\)/);
|
|
65
|
+
if (defaultMatch) {
|
|
66
|
+
defaultValue = defaultMatch[1];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const isRelation = line.includes('@relation') ||
|
|
70
|
+
(!!fieldName &&
|
|
71
|
+
(fieldName.endsWith('_id') || fieldName === 'userId' || fieldName === 'user_id'));
|
|
72
|
+
// Check for inline @enableSearch comment
|
|
73
|
+
if (line.includes('// @enableSearch')) {
|
|
74
|
+
searchFields.push({
|
|
75
|
+
name: fieldName || '',
|
|
76
|
+
type: fieldType || '',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (fieldName && fieldType) {
|
|
80
|
+
fields.push({
|
|
81
|
+
name: fieldName,
|
|
82
|
+
type: fieldType,
|
|
83
|
+
isRequired: false,
|
|
84
|
+
isOptional,
|
|
85
|
+
isId,
|
|
86
|
+
isUnique: false,
|
|
87
|
+
isUpdatedAt,
|
|
88
|
+
isCreatedAt,
|
|
89
|
+
hasDefaultValue,
|
|
90
|
+
defaultValue, // Add the extracted default value
|
|
91
|
+
isRelation,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Check for model-level @enableSearch before the model definition
|
|
97
|
+
if (schema.includes(`// @enableSearch\nmodel ${modelName}`)) {
|
|
98
|
+
// Add all string fields as searchable
|
|
99
|
+
fields.forEach((field) => {
|
|
100
|
+
if (field.type.toLowerCase() === 'string' &&
|
|
101
|
+
!searchFields.some((sf) => sf.name === field.name)) {
|
|
102
|
+
searchFields.push({
|
|
103
|
+
name: field.name,
|
|
104
|
+
type: field.type,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
models.push({
|
|
110
|
+
name: modelName,
|
|
111
|
+
mappedName: mappedName || '',
|
|
112
|
+
fields,
|
|
113
|
+
searchFields: searchFields.length > 0 ? searchFields : undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return models;
|
|
117
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "suparisma",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/index.js",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"generate-prisma-types": "prisma generate",
|
|
11
|
+
"generate-hooks": "ts-node --transpile-only src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@supabase/supabase-js": "^2.49.4",
|
|
15
|
+
"dotenv": "^16.5.0",
|
|
16
|
+
"fs": "0.0.1-security",
|
|
17
|
+
"path": "^0.12.7",
|
|
18
|
+
"pg": "^8.16.0",
|
|
19
|
+
"react": "^19.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.15.18",
|
|
23
|
+
"@types/pg": "^8.15.2",
|
|
24
|
+
"@types/react": "^19.1.4",
|
|
25
|
+
"prisma": "^6.8.2",
|
|
26
|
+
"supabase-js": "link:@types/@supabase/supabase-js"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// This is your Prisma schema file,
|
|
2
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
5
|
+
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
6
|
+
|
|
7
|
+
generator client {
|
|
8
|
+
provider = "prisma-client-js"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
datasource db {
|
|
12
|
+
provider = "postgresql"
|
|
13
|
+
url = env("DATABASE_URL")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model User {
|
|
17
|
+
id String @id @default(uuid())
|
|
18
|
+
email String @unique
|
|
19
|
+
name String?
|
|
20
|
+
createdAt DateTime @default(now())
|
|
21
|
+
updatedAt DateTime @updatedAt
|
|
22
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Configuration
|
|
2
|
+
export const PRISMA_SCHEMA_PATH = process.env.SUPARISMA_PRISMA_SCHEMA_PATH || 'prisma/schema.prisma';
|
|
3
|
+
export const OUTPUT_DIR = process.env.SUPARISMA_OUTPUT_DIR || 'src/suparisma/generated';
|
|
4
|
+
export const TYPES_DIR = `${OUTPUT_DIR}/types`;
|
|
5
|
+
export const HOOKS_DIR = `${OUTPUT_DIR}/hooks`;
|
|
6
|
+
export const UTILS_DIR = `${OUTPUT_DIR}/utils`;
|
|
7
|
+
export const HOOK_NAME_PREFIX = 'useSuparisma';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
2
|
+
// Edit the generator script instead: scripts/generate-realtime-hooks.ts
|
|
3
|
+
|
|
4
|
+
import { createSuparismaHook } from '../utils/core';
|
|
5
|
+
import type {
|
|
6
|
+
UserWithRelations,
|
|
7
|
+
UserCreateInput,
|
|
8
|
+
UserUpdateInput,
|
|
9
|
+
UserWhereInput,
|
|
10
|
+
UserWhereUniqueInput,
|
|
11
|
+
UserOrderByInput,
|
|
12
|
+
UserHookApi,
|
|
13
|
+
UseUserOptions
|
|
14
|
+
} from '../types/UserTypes';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A Prisma-like hook for interacting with User records with real-time capabilities.
|
|
18
|
+
*
|
|
19
|
+
* This hook provides CRUD operations, real-time updates, and search functionality.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Optional configuration options for the hook
|
|
22
|
+
* @returns An object with data state and methods for interacting with User records
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Basic usage - get all User records with realtime updates
|
|
26
|
+
* const user = useSuparismaUser();
|
|
27
|
+
* const { data, loading, error } = user;
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // With filtering and ordering
|
|
31
|
+
* const user = useSuparismaUser({
|
|
32
|
+
* where: { active: true },
|
|
33
|
+
* orderBy: { created_at: 'desc' },
|
|
34
|
+
* limit: 10
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Create a new record
|
|
39
|
+
* const result = await user.create({
|
|
40
|
+
* name: "Example Name",
|
|
41
|
+
* // other fields...
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Update a record
|
|
46
|
+
* const result = await user.update({
|
|
47
|
+
* where: { id: "123" },
|
|
48
|
+
* data: { name: "Updated Name" }
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Delete a record
|
|
53
|
+
* const result = await user.delete({ id: "123" });
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Find records with specific criteria
|
|
57
|
+
* const result = await user.findMany({
|
|
58
|
+
* where: { // filters },
|
|
59
|
+
* orderBy: { // ordering },
|
|
60
|
+
* take: 20 // limit
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
export const useSuparismaUser = createSuparismaHook<
|
|
64
|
+
UserWithRelations,
|
|
65
|
+
UserWithRelations,
|
|
66
|
+
UserCreateInput,
|
|
67
|
+
UserUpdateInput,
|
|
68
|
+
UserWhereInput,
|
|
69
|
+
UserWhereUniqueInput,
|
|
70
|
+
UserOrderByInput
|
|
71
|
+
>({
|
|
72
|
+
tableName: 'User',
|
|
73
|
+
hasCreatedAt: true,
|
|
74
|
+
hasUpdatedAt: true,
|
|
75
|
+
// Default values from schema
|
|
76
|
+
defaultValues: {"id":"uuid(","createdAt":"now("}
|
|
77
|
+
}) as (options?: UseUserOptions) => UserHookApi;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
2
|
+
// Edit the generator script instead: scripts/generate-realtime-hooks.ts
|
|
3
|
+
|
|
4
|
+
import { useSuparismaUser } from './hooks/useSuparismaUser';
|
|
5
|
+
import type { UseUserOptions, UserHookApi } from './types/UserTypes';
|
|
6
|
+
export type { SuparismaOptions, SearchQuery, SearchState, FilterOperators } from './utils/core';
|
|
7
|
+
export type { UserWithRelations, UserCreateInput, UserUpdateInput, UserWhereInput, UserWhereUniqueInput, UserOrderByInput, UserHookApi, UseUserOptions } from './types/UserTypes';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interface for all Suparisma hooks with dot notation access.
|
|
11
|
+
* This provides IntelliSense for all available models.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Access hooks for different models
|
|
15
|
+
* const users = useSuparisma.user();
|
|
16
|
+
* const posts = useSuparisma.post();
|
|
17
|
+
*/
|
|
18
|
+
export interface SuparismaHooks {
|
|
19
|
+
user: (options?: UseUserOptions) => UserHookApi;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main Suparisma hook object with dot notation access to all model hooks.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Get hooks for different models
|
|
27
|
+
* import useSuparisma from './generated';
|
|
28
|
+
*
|
|
29
|
+
* // Access user model with all hook methods
|
|
30
|
+
* const users = useSuparisma.user();
|
|
31
|
+
* const { data, loading, error } = users;
|
|
32
|
+
*
|
|
33
|
+
* // Create a new record
|
|
34
|
+
* await users.create({ name: "John" });
|
|
35
|
+
*
|
|
36
|
+
* // Delete a record
|
|
37
|
+
* await users.delete({ id: "123" });
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Use with filtering and options
|
|
41
|
+
* const admins = useSuparisma.user({
|
|
42
|
+
* where: { role: 'admin' },
|
|
43
|
+
* orderBy: { created_at: 'desc' }
|
|
44
|
+
* });
|
|
45
|
+
*/
|
|
46
|
+
const useSuparisma: SuparismaHooks = {
|
|
47
|
+
user: useSuparismaUser,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default useSuparisma;
|