@ismaelsoilet/synapsejs 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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/bin/synapse.ts +919 -0
  4. package/package.json +70 -0
  5. package/src/client/components.tsx +508 -0
  6. package/src/client/context.tsx +62 -0
  7. package/src/client/hooks.ts +152 -0
  8. package/src/client/i18n.ts +40 -0
  9. package/src/client.ts +66 -0
  10. package/src/compiler/agent-diagnostic-json.ts +113 -0
  11. package/src/compiler/ast-daemon-compressor.ts +190 -0
  12. package/src/compiler/client-bundler.ts +356 -0
  13. package/src/compiler/context-bench.ts +191 -0
  14. package/src/compiler/contract.ts +260 -0
  15. package/src/compiler/db-schema-generator.ts +306 -0
  16. package/src/compiler/fields-parser.ts +267 -0
  17. package/src/compiler/impact-analyzer.ts +279 -0
  18. package/src/compiler/migration-runner.ts +555 -0
  19. package/src/compiler/oracle-runner.ts +293 -0
  20. package/src/compiler/scaffolder.ts +528 -0
  21. package/src/compiler/schema-dag.ts +137 -0
  22. package/src/compiler/schema-drift.ts +184 -0
  23. package/src/compiler/slice-discovery.ts +154 -0
  24. package/src/compiler/slice-splitter.ts +838 -0
  25. package/src/compiler/slice-templates.ts +1527 -0
  26. package/src/compiler/standalone-builder.ts +117 -0
  27. package/src/core/action-context.ts +152 -0
  28. package/src/core/config.ts +77 -0
  29. package/src/core/database-client.ts +246 -0
  30. package/src/core/database-factory.ts +46 -0
  31. package/src/core/image-optimizer.ts +83 -0
  32. package/src/core/index.ts +14 -0
  33. package/src/core/jobs.ts +53 -0
  34. package/src/core/machine-types.ts +75 -0
  35. package/src/core/oracle.ts +20 -0
  36. package/src/core/postgres-client.ts +164 -0
  37. package/src/core/postgres-queue.ts +288 -0
  38. package/src/core/query-builder.ts +343 -0
  39. package/src/core/rpc-client.ts +70 -0
  40. package/src/core/session-context.ts +87 -0
  41. package/src/core/session-cookie.ts +81 -0
  42. package/src/core/session-token.ts +170 -0
  43. package/src/core/sqlite-client.ts +201 -0
  44. package/src/core/storage.ts +317 -0
  45. package/src/index.ts +133 -0
  46. package/src/mcp/server.ts +553 -0
  47. package/src/runtime/client-entry.ts +65 -0
  48. package/src/runtime/discovery-rules.ts +63 -0
  49. package/src/runtime/event-hub.ts +97 -0
  50. package/src/runtime/postgres-event-hub.ts +148 -0
  51. package/src/runtime/queue-engine.ts +255 -0
  52. package/src/runtime/rate-limiter.ts +152 -0
  53. package/src/runtime/server.ts +1550 -0
  54. package/src/runtime/uploads.ts +192 -0
  55. package/templates/starter/.dockerignore +7 -0
  56. package/templates/starter/Dockerfile +40 -0
  57. package/templates/starter/README.md +40 -0
  58. package/templates/starter/gitignore +6 -0
  59. package/templates/starter/package.json +34 -0
  60. package/templates/starter/src/slices/welcome/hello-world.slice.tsx +172 -0
  61. package/templates/starter/tsconfig.json +21 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * SynapseJS - Declarative AST Slice Migration Runner
3
+ *
4
+ * Auto-discovers 'sliceSchema' DDL definitions co-located in *.slice.tsx files
5
+ * and applies them idempotently to the active database (SQLite or PostgreSQL).
6
+ * Preserves Locality of Behavior (LoB): each slice owns its own table contracts!
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as ts from 'typescript';
12
+ import type { DatabaseClient } from '../core/database-client';
13
+ import { getDatabase } from '../core/index';
14
+ import { PostgresDatabaseClient } from '../core/postgres-client';
15
+ import { orderSlicesByDag } from './schema-dag';
16
+ import { findSliceFiles, resolveSlicesDir, type SlicesDirErrorCode } from './slice-discovery';
17
+
18
+ export interface MigrationResult {
19
+ slice: string;
20
+ filePath: string;
21
+ status: 'APPLIED' | 'SKIPPED' | 'FAILED';
22
+ /** Statements actually executed in this run (0 for a skipped slice). */
23
+ statementsApplied: number;
24
+ error?: string;
25
+ }
26
+
27
+ export interface MigrationReport {
28
+ status: 'PASS' | 'FAIL';
29
+ totalDiscovered: number;
30
+ appliedCount: number;
31
+ skippedCount: number;
32
+ statementsApplied: number;
33
+ migrations: MigrationResult[];
34
+ code?: SlicesDirErrorCode;
35
+ message?: string;
36
+ candidates?: string[];
37
+ }
38
+
39
+ export interface BidirectionalDdl {
40
+ upDdl: string;
41
+ downDdl: string | null;
42
+ }
43
+
44
+ export interface RollbackResult {
45
+ slice: string;
46
+ status: 'ROLLED_BACK' | 'FAILED' | 'SKIPPED';
47
+ statementsRolledBack: number;
48
+ error?: string;
49
+ }
50
+
51
+ export interface RollbackReport {
52
+ status: 'PASS' | 'FAIL';
53
+ totalRolledBack: number;
54
+ rollbacks: RollbackResult[];
55
+ message?: string;
56
+ }
57
+
58
+ export interface RollbackOptions {
59
+ targetSlice?: string;
60
+ steps?: number;
61
+ }
62
+
63
+ export function parseBidirectionalDdl(rawDdl: string): BidirectionalDdl {
64
+ const downMarkerRegex = /^[ \t]*--\s*down:?[ \t]*$/im;
65
+ const match = downMarkerRegex.exec(rawDdl);
66
+ if (!match) {
67
+ return { upDdl: rawDdl.trim(), downDdl: null };
68
+ }
69
+ const splitIndex = match.index;
70
+ const upDdl = rawDdl.slice(0, splitIndex).trim();
71
+ const downDdl = rawDdl.slice(splitIndex + match[0].length).trim();
72
+ return {
73
+ upDdl,
74
+ downDdl: downDdl.length > 0 ? downDdl : null
75
+ };
76
+ }
77
+
78
+ function hashStatement(statement: string): string {
79
+ const hasher = new Bun.CryptoHasher('sha256');
80
+ hasher.update(statement);
81
+ return hasher.digest('hex');
82
+ }
83
+
84
+ export function splitStatements(ddl: string): string[] {
85
+ const statements: string[] = [];
86
+ let current = '';
87
+ let inSingleQuote = false;
88
+ let inDoubleQuote = false;
89
+ let inLineComment = false;
90
+ let inBlockComment = false;
91
+ let dollarTag: string | null = null;
92
+
93
+ let i = 0;
94
+ while (i < ddl.length) {
95
+ const char = ddl[i];
96
+ const next = ddl[i + 1] ?? '';
97
+
98
+ // Handle line comment (-- ...)
99
+ if (inLineComment) {
100
+ current += char;
101
+ if (char === '\n') {
102
+ inLineComment = false;
103
+ }
104
+ i++;
105
+ continue;
106
+ }
107
+
108
+ // Handle block comment (/* ... */)
109
+ if (inBlockComment) {
110
+ current += char;
111
+ if (char === '*' && next === '/') {
112
+ current += next;
113
+ inBlockComment = false;
114
+ i += 2;
115
+ continue;
116
+ }
117
+ i++;
118
+ continue;
119
+ }
120
+
121
+ // Handle single quote string ('...')
122
+ if (inSingleQuote) {
123
+ current += char;
124
+ if (char === '\\') {
125
+ current += next;
126
+ i += 2;
127
+ continue;
128
+ }
129
+ if (char === "'") {
130
+ if (next === "'") {
131
+ current += next;
132
+ i += 2;
133
+ continue;
134
+ }
135
+ inSingleQuote = false;
136
+ }
137
+ i++;
138
+ continue;
139
+ }
140
+
141
+ // Handle double quote identifier ("...")
142
+ if (inDoubleQuote) {
143
+ current += char;
144
+ if (char === '"') {
145
+ if (next === '"') {
146
+ current += next;
147
+ i += 2;
148
+ continue;
149
+ }
150
+ inDoubleQuote = false;
151
+ }
152
+ i++;
153
+ continue;
154
+ }
155
+
156
+ // Handle dollar-quoted string ($tag$ ... $tag$)
157
+ if (dollarTag !== null) {
158
+ current += char;
159
+ if (char === '$' && ddl.startsWith(dollarTag, i)) {
160
+ current += dollarTag.slice(1);
161
+ i += dollarTag.length;
162
+ dollarTag = null;
163
+ continue;
164
+ }
165
+ i++;
166
+ continue;
167
+ }
168
+
169
+ // Check transitions from normal state:
170
+ if (char === '-' && next === '-') {
171
+ inLineComment = true;
172
+ current += '--';
173
+ i += 2;
174
+ continue;
175
+ }
176
+
177
+ if (char === '/' && next === '*') {
178
+ inBlockComment = true;
179
+ current += '/*';
180
+ i += 2;
181
+ continue;
182
+ }
183
+
184
+ if (char === "'") {
185
+ inSingleQuote = true;
186
+ current += "'";
187
+ i++;
188
+ continue;
189
+ }
190
+
191
+ if (char === '"') {
192
+ inDoubleQuote = true;
193
+ current += '"';
194
+ i++;
195
+ continue;
196
+ }
197
+
198
+ if (char === '$') {
199
+ const match = ddl.slice(i).match(/^\$[A-Za-z0-9_]*\$/);
200
+ if (match) {
201
+ dollarTag = match[0];
202
+ current += dollarTag;
203
+ i += dollarTag.length;
204
+ continue;
205
+ }
206
+ }
207
+
208
+ // Statement terminator outside strings or comments
209
+ if (char === ';') {
210
+ const trimmed = current.trim();
211
+ if (trimmed.length > 0) {
212
+ statements.push(trimmed);
213
+ }
214
+ current = '';
215
+ i++;
216
+ continue;
217
+ }
218
+
219
+ current += char;
220
+ i++;
221
+ }
222
+
223
+ const trailing = current.trim();
224
+ if (trailing.length > 0) {
225
+ statements.push(trailing);
226
+ }
227
+
228
+ return statements;
229
+ }
230
+
231
+ export function extractSliceSchema(sourceFile: ts.SourceFile): string | null {
232
+ let ddl: string | null = null;
233
+ ts.forEachChild(sourceFile, (node) => {
234
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
235
+ const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
236
+
237
+ if (isExported && ts.isVariableStatement(node)) {
238
+ for (const decl of node.declarationList.declarations) {
239
+ const varName = decl.name.getText(sourceFile);
240
+ if (varName === 'sliceSchema' && decl.initializer) {
241
+ if (ts.isStringLiteral(decl.initializer) || ts.isNoSubstitutionTemplateLiteral(decl.initializer)) {
242
+ ddl = decl.initializer.text.trim();
243
+ } else if (ts.isTemplateExpression(decl.initializer)) {
244
+ let fullText = decl.initializer.head.text;
245
+ for (const span of decl.initializer.templateSpans) {
246
+ if (ts.isStringLiteral(span.expression) || ts.isNoSubstitutionTemplateLiteral(span.expression)) {
247
+ fullText += span.expression.text;
248
+ } else {
249
+ fullText += span.expression.getText(sourceFile).replace(/^['"`]|['"`]$/g, '');
250
+ }
251
+ fullText += span.literal.text;
252
+ }
253
+ ddl = fullText.trim();
254
+ }
255
+ }
256
+ }
257
+ }
258
+ });
259
+ return ddl;
260
+ }
261
+
262
+ export async function runSliceMigrations(
263
+ baseDir: string = process.cwd(),
264
+ customDb?: DatabaseClient
265
+ ): Promise<MigrationReport> {
266
+ const db = customDb || getDatabase();
267
+ const isPostgres = db instanceof PostgresDatabaseClient;
268
+ const SYNAPSE_MIGRATION_LOCK_ID = 82910471;
269
+
270
+ if (isPostgres) {
271
+ await db.query(`SELECT pg_advisory_lock(${SYNAPSE_MIGRATION_LOCK_ID});`);
272
+ }
273
+
274
+ try {
275
+ // Ensure migrations tracking table exists.
276
+ // TIMESTAMP is accepted by both SQLite and PostgreSQL; DATETIME is SQLite-only.
277
+ await db.query(`
278
+ CREATE TABLE IF NOT EXISTS _synapse_migrations (
279
+ slice_name TEXT PRIMARY KEY,
280
+ schema_hash TEXT NOT NULL,
281
+ down_ddl TEXT,
282
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
283
+ );
284
+ `);
285
+
286
+ try {
287
+ await db.query(`ALTER TABLE _synapse_migrations ADD COLUMN down_ddl TEXT;`);
288
+ } catch (_) {}
289
+
290
+ // Progresso por statement, não por fatia: sem isso, um `ALTER TABLE` registrado
291
+ // uma vez volta a rodar na próxima edição do DDL e falha para sempre
292
+ // ("duplicate column name"). Com o histórico por statement, cada um roda uma
293
+ // única vez e evoluir o schema passa a ser só declarar a mudança.
294
+ await db.query(`
295
+ CREATE TABLE IF NOT EXISTS _synapse_migration_statements (
296
+ slice_name TEXT NOT NULL,
297
+ statement_hash TEXT NOT NULL,
298
+ down_statement TEXT,
299
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
300
+ PRIMARY KEY (slice_name, statement_hash)
301
+ );
302
+ `);
303
+
304
+ try {
305
+ await db.query(`ALTER TABLE _synapse_migration_statements ADD COLUMN down_statement TEXT;`);
306
+ } catch (_) {}
307
+
308
+ const resolution = resolveSlicesDir(baseDir);
309
+ if (!resolution.ok) {
310
+ return {
311
+ status: 'FAIL',
312
+ totalDiscovered: 0,
313
+ appliedCount: 0,
314
+ skippedCount: 0,
315
+ statementsApplied: 0,
316
+ migrations: [],
317
+ code: resolution.error.code,
318
+ message: resolution.error.message,
319
+ candidates: resolution.error.candidates
320
+ };
321
+ }
322
+
323
+ const sliceFiles = orderSlicesByDag(findSliceFiles(resolution.value.slicesDir));
324
+ const migrations: MigrationResult[] = [];
325
+ let appliedCount = 0;
326
+ let skippedCount = 0;
327
+ let statementsAppliedTotal = 0;
328
+ let hasFailure = false;
329
+
330
+ for (const filePath of sliceFiles) {
331
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
332
+ const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
333
+
334
+ const sliceName = path.basename(filePath, '.slice.tsx');
335
+ const ddlContent = extractSliceSchema(sourceFile);
336
+
337
+ if (!ddlContent) {
338
+ continue;
339
+ }
340
+
341
+ const { upDdl, downDdl } = parseBidirectionalDdl(ddlContent);
342
+ if (!upDdl) {
343
+ continue;
344
+ }
345
+
346
+ const hash = hashStatement(upDdl);
347
+ const statements = splitStatements(upDdl);
348
+ const withHashes = statements.map((statement) => ({ statement, hash: hashStatement(statement) }));
349
+
350
+ const applied = await db.query<{ statement_hash: string }>(
351
+ `SELECT statement_hash FROM _synapse_migration_statements WHERE slice_name = $1`,
352
+ [sliceName]
353
+ );
354
+ const appliedHashes = new Set(applied.map((row) => row.statement_hash));
355
+ const pending = withHashes.filter((entry) => !appliedHashes.has(entry.hash));
356
+
357
+ if (pending.length === 0) {
358
+ skippedCount++;
359
+ migrations.push({
360
+ slice: sliceName,
361
+ filePath: path.relative(baseDir, filePath),
362
+ status: 'SKIPPED',
363
+ statementsApplied: 0
364
+ });
365
+ continue;
366
+ }
367
+
368
+ try {
369
+ await db.transaction(async (tx) => {
370
+ for (const entry of pending) {
371
+ await tx.query(entry.statement);
372
+ }
373
+ for (const entry of pending) {
374
+ await tx.query(
375
+ `INSERT INTO _synapse_migration_statements (slice_name, statement_hash, down_statement) VALUES ($1, $2, $3)
376
+ ON CONFLICT(slice_name, statement_hash) DO NOTHING`,
377
+ [sliceName, entry.hash, downDdl]
378
+ );
379
+ }
380
+ await tx.query(
381
+ `INSERT INTO _synapse_migrations (slice_name, schema_hash, down_ddl, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
382
+ ON CONFLICT(slice_name) DO UPDATE SET schema_hash = $2, down_ddl = $3, applied_at = CURRENT_TIMESTAMP`,
383
+ [sliceName, hash, downDdl]
384
+ );
385
+ });
386
+
387
+ appliedCount++;
388
+ statementsAppliedTotal += pending.length;
389
+ migrations.push({
390
+ slice: sliceName,
391
+ filePath: path.relative(baseDir, filePath),
392
+ status: 'APPLIED',
393
+ statementsApplied: pending.length
394
+ });
395
+ } catch (err: any) {
396
+ hasFailure = true;
397
+ migrations.push({
398
+ slice: sliceName,
399
+ filePath: path.relative(baseDir, filePath),
400
+ status: 'FAILED',
401
+ statementsApplied: 0,
402
+ error: err.message
403
+ });
404
+ }
405
+ }
406
+
407
+ return {
408
+ status: hasFailure ? 'FAIL' : 'PASS',
409
+ totalDiscovered: migrations.length,
410
+ appliedCount,
411
+ skippedCount,
412
+ statementsApplied: statementsAppliedTotal,
413
+ migrations
414
+ };
415
+ } finally {
416
+ if (isPostgres) {
417
+ await db.query(`SELECT pg_advisory_unlock(${SYNAPSE_MIGRATION_LOCK_ID});`);
418
+ }
419
+ }
420
+ }
421
+
422
+ export async function rollbackSliceMigrations(
423
+ _baseDir: string = process.cwd(),
424
+ customDb?: DatabaseClient,
425
+ options: RollbackOptions = {}
426
+ ): Promise<RollbackReport> {
427
+ const db = customDb || getDatabase();
428
+ const isPostgres = db instanceof PostgresDatabaseClient;
429
+ const SYNAPSE_MIGRATION_LOCK_ID = 82910471;
430
+
431
+ if (isPostgres) {
432
+ await db.query(`SELECT pg_advisory_lock(${SYNAPSE_MIGRATION_LOCK_ID});`);
433
+ }
434
+
435
+ try {
436
+ const tableExists = await db.query(
437
+ isPostgres
438
+ ? `SELECT 1 FROM information_schema.tables WHERE table_name = '_synapse_migrations'`
439
+ : `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_synapse_migrations'`
440
+ );
441
+
442
+ if (tableExists.length === 0) {
443
+ return {
444
+ status: 'PASS',
445
+ totalRolledBack: 0,
446
+ rollbacks: [],
447
+ message: 'Nenhuma tabela de migrações encontrada no banco.'
448
+ };
449
+ }
450
+
451
+ let records: Array<{ slice_name: string; down_ddl: string | null }>;
452
+ if (options.targetSlice) {
453
+ const cleanTarget = options.targetSlice.endsWith('.slice.tsx')
454
+ ? path.basename(options.targetSlice, '.slice.tsx')
455
+ : options.targetSlice.includes('/')
456
+ ? (options.targetSlice.split('/').pop() ?? options.targetSlice)
457
+ : options.targetSlice;
458
+
459
+ records = await db.query<{ slice_name: string; down_ddl: string | null }>(
460
+ `SELECT slice_name, down_ddl FROM _synapse_migrations WHERE slice_name = $1`,
461
+ [cleanTarget]
462
+ );
463
+
464
+ if (records.length === 0) {
465
+ return {
466
+ status: 'FAIL',
467
+ totalRolledBack: 0,
468
+ rollbacks: [
469
+ {
470
+ slice: cleanTarget,
471
+ status: 'FAILED',
472
+ statementsRolledBack: 0,
473
+ error: `Fatia '${cleanTarget}' não foi encontrada no histórico de migrações aplicadas.`
474
+ }
475
+ ]
476
+ };
477
+ }
478
+ } else {
479
+ const steps = Math.max(1, options.steps ?? 1);
480
+ records = await db.query<{ slice_name: string; down_ddl: string | null }>(
481
+ `SELECT slice_name, down_ddl FROM _synapse_migrations ORDER BY applied_at DESC, slice_name DESC LIMIT $1`,
482
+ [steps]
483
+ );
484
+ }
485
+
486
+ if (records.length === 0) {
487
+ return {
488
+ status: 'PASS',
489
+ totalRolledBack: 0,
490
+ rollbacks: [],
491
+ message: 'Nenhuma migração disponível para reverter.'
492
+ };
493
+ }
494
+
495
+ const rollbacks: RollbackResult[] = [];
496
+ let hasFailure = false;
497
+ let totalRolledBack = 0;
498
+
499
+ for (const record of records) {
500
+ if (!record.down_ddl || record.down_ddl.trim().length === 0) {
501
+ hasFailure = true;
502
+ rollbacks.push({
503
+ slice: record.slice_name,
504
+ status: 'FAILED',
505
+ statementsRolledBack: 0,
506
+ error: `A fatia '${record.slice_name}' não possui bloco '-- down:' declarado para reversão.`
507
+ });
508
+ continue;
509
+ }
510
+
511
+ const downStatements = splitStatements(record.down_ddl);
512
+ try {
513
+ await db.transaction(async (tx) => {
514
+ for (const stmt of downStatements) {
515
+ await tx.query(stmt);
516
+ }
517
+ await tx.query(`DELETE FROM _synapse_migration_statements WHERE slice_name = $1`, [record.slice_name]);
518
+ await tx.query(`DELETE FROM _synapse_migrations WHERE slice_name = $1`, [record.slice_name]);
519
+ });
520
+
521
+ totalRolledBack++;
522
+ rollbacks.push({
523
+ slice: record.slice_name,
524
+ status: 'ROLLED_BACK',
525
+ statementsRolledBack: downStatements.length
526
+ });
527
+ } catch (err: unknown) {
528
+ hasFailure = true;
529
+ rollbacks.push({
530
+ slice: record.slice_name,
531
+ status: 'FAILED',
532
+ statementsRolledBack: 0,
533
+ error: err instanceof Error ? err.message : String(err)
534
+ });
535
+ }
536
+ }
537
+
538
+ return {
539
+ status: hasFailure ? 'FAIL' : 'PASS',
540
+ totalRolledBack,
541
+ rollbacks
542
+ };
543
+ } finally {
544
+ if (isPostgres) {
545
+ await db.query(`SELECT pg_advisory_unlock(${SYNAPSE_MIGRATION_LOCK_ID});`);
546
+ }
547
+ }
548
+ }
549
+
550
+ if (import.meta.main) {
551
+ console.log('⚡ [Synapse Migrator] Varrendo fatias verticais para auto-descoberta de DDL...');
552
+ const report = await runSliceMigrations();
553
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
554
+ process.exit(report.status === 'PASS' ? 0 : 1);
555
+ }