@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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/synapse.ts +919 -0
- package/package.json +70 -0
- package/src/client/components.tsx +508 -0
- package/src/client/context.tsx +62 -0
- package/src/client/hooks.ts +152 -0
- package/src/client/i18n.ts +40 -0
- package/src/client.ts +66 -0
- package/src/compiler/agent-diagnostic-json.ts +113 -0
- package/src/compiler/ast-daemon-compressor.ts +190 -0
- package/src/compiler/client-bundler.ts +356 -0
- package/src/compiler/context-bench.ts +191 -0
- package/src/compiler/contract.ts +260 -0
- package/src/compiler/db-schema-generator.ts +306 -0
- package/src/compiler/fields-parser.ts +267 -0
- package/src/compiler/impact-analyzer.ts +279 -0
- package/src/compiler/migration-runner.ts +555 -0
- package/src/compiler/oracle-runner.ts +293 -0
- package/src/compiler/scaffolder.ts +528 -0
- package/src/compiler/schema-dag.ts +137 -0
- package/src/compiler/schema-drift.ts +184 -0
- package/src/compiler/slice-discovery.ts +154 -0
- package/src/compiler/slice-splitter.ts +838 -0
- package/src/compiler/slice-templates.ts +1527 -0
- package/src/compiler/standalone-builder.ts +117 -0
- package/src/core/action-context.ts +152 -0
- package/src/core/config.ts +77 -0
- package/src/core/database-client.ts +246 -0
- package/src/core/database-factory.ts +46 -0
- package/src/core/image-optimizer.ts +83 -0
- package/src/core/index.ts +14 -0
- package/src/core/jobs.ts +53 -0
- package/src/core/machine-types.ts +75 -0
- package/src/core/oracle.ts +20 -0
- package/src/core/postgres-client.ts +164 -0
- package/src/core/postgres-queue.ts +288 -0
- package/src/core/query-builder.ts +343 -0
- package/src/core/rpc-client.ts +70 -0
- package/src/core/session-context.ts +87 -0
- package/src/core/session-cookie.ts +81 -0
- package/src/core/session-token.ts +170 -0
- package/src/core/sqlite-client.ts +201 -0
- package/src/core/storage.ts +317 -0
- package/src/index.ts +133 -0
- package/src/mcp/server.ts +553 -0
- package/src/runtime/client-entry.ts +65 -0
- package/src/runtime/discovery-rules.ts +63 -0
- package/src/runtime/event-hub.ts +97 -0
- package/src/runtime/postgres-event-hub.ts +148 -0
- package/src/runtime/queue-engine.ts +255 -0
- package/src/runtime/rate-limiter.ts +152 -0
- package/src/runtime/server.ts +1550 -0
- package/src/runtime/uploads.ts +192 -0
- package/templates/starter/.dockerignore +7 -0
- package/templates/starter/Dockerfile +40 -0
- package/templates/starter/README.md +40 -0
- package/templates/starter/gitignore +6 -0
- package/templates/starter/package.json +34 -0
- package/templates/starter/src/slices/welcome/hello-world.slice.tsx +172 -0
- package/templates/starter/tsconfig.json +21 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Schema Drift Detector
|
|
3
|
+
*
|
|
4
|
+
* Compares live database catalog (SQLite or PostgreSQL) against the declared
|
|
5
|
+
* AST sliceSchema DDL contracts across the project.
|
|
6
|
+
*
|
|
7
|
+
* Pinpoints:
|
|
8
|
+
* - Missing tables (declared in slice, not yet migrated)
|
|
9
|
+
* - Missing columns (declared in slice, missing in DB)
|
|
10
|
+
* - Orphan tables (exist in DB, not declared by any slice)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DatabaseClient } from '../core/database-client';
|
|
14
|
+
import { getDatabase } from '../core/database-factory';
|
|
15
|
+
import { PostgresDatabaseClient } from '../core/postgres-client';
|
|
16
|
+
import { type DatabaseCatalog, generateDatabaseSchemaCatalog } from './db-schema-generator';
|
|
17
|
+
|
|
18
|
+
export interface MissingTableInfo {
|
|
19
|
+
table: string;
|
|
20
|
+
sourceSlice?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MissingColumnInfo {
|
|
24
|
+
table: string;
|
|
25
|
+
column: string;
|
|
26
|
+
expectedType: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SchemaDriftReport {
|
|
30
|
+
status: 'PASS' | 'DRIFT_DETECTED';
|
|
31
|
+
totalDeclaredTables: number;
|
|
32
|
+
totalLiveTables: number;
|
|
33
|
+
drift: {
|
|
34
|
+
missingTables: MissingTableInfo[];
|
|
35
|
+
missingColumns: MissingColumnInfo[];
|
|
36
|
+
orphanTables: string[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const IGNORED_TABLE_PATTERNS = [/^sqlite_/i, /^_synapse_/i, /^pg_/i, /^sql_/i];
|
|
41
|
+
|
|
42
|
+
function isIgnoredTable(name: string): boolean {
|
|
43
|
+
return IGNORED_TABLE_PATTERNS.some((pat) => pat.test(name));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface LiveColumn {
|
|
47
|
+
name: string;
|
|
48
|
+
type: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface LiveTableSchema {
|
|
52
|
+
name: string;
|
|
53
|
+
columns: Map<string, LiveColumn>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function inspectLiveDatabase(db: DatabaseClient): Promise<Map<string, LiveTableSchema>> {
|
|
57
|
+
const isPostgres = db instanceof PostgresDatabaseClient;
|
|
58
|
+
const liveSchema = new Map<string, LiveTableSchema>();
|
|
59
|
+
|
|
60
|
+
if (isPostgres) {
|
|
61
|
+
const tables = await db.query<{ table_name: string }>(`
|
|
62
|
+
SELECT table_name
|
|
63
|
+
FROM information_schema.tables
|
|
64
|
+
WHERE table_schema = 'public'
|
|
65
|
+
AND table_type = 'BASE TABLE'
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
for (const t of tables) {
|
|
69
|
+
const tName = t.table_name.toLowerCase();
|
|
70
|
+
if (isIgnoredTable(tName)) continue;
|
|
71
|
+
liveSchema.set(tName, { name: tName, columns: new Map() });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const columns = await db.query<{ table_name: string; column_name: string; data_type: string }>(`
|
|
75
|
+
SELECT table_name, column_name, data_type
|
|
76
|
+
FROM information_schema.columns
|
|
77
|
+
WHERE table_schema = 'public'
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
for (const col of columns) {
|
|
81
|
+
const tName = col.table_name.toLowerCase();
|
|
82
|
+
const colName = col.column_name.toLowerCase();
|
|
83
|
+
const tbl = liveSchema.get(tName);
|
|
84
|
+
if (tbl) {
|
|
85
|
+
tbl.columns.set(colName, {
|
|
86
|
+
name: col.column_name,
|
|
87
|
+
type: col.data_type
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// SQLite
|
|
93
|
+
const tables = await db.query<{ name: string }>(`
|
|
94
|
+
SELECT name
|
|
95
|
+
FROM sqlite_master
|
|
96
|
+
WHERE type = 'table'
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
for (const t of tables) {
|
|
100
|
+
const tName = t.name.toLowerCase();
|
|
101
|
+
if (isIgnoredTable(tName)) continue;
|
|
102
|
+
|
|
103
|
+
const colMap = new Map<string, LiveColumn>();
|
|
104
|
+
const colInfo = await db.query<{ name: string; type: string }>(`
|
|
105
|
+
PRAGMA table_info("${t.name}");
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
for (const c of colInfo) {
|
|
109
|
+
colMap.set(c.name.toLowerCase(), {
|
|
110
|
+
name: c.name,
|
|
111
|
+
type: c.type
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
liveSchema.set(tName, { name: t.name, columns: colMap });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return liveSchema;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Checks for schema drift between AST slice schemas and a live database.
|
|
124
|
+
*/
|
|
125
|
+
export async function checkSchemaDrift(
|
|
126
|
+
rootDir: string = process.cwd(),
|
|
127
|
+
customDb?: DatabaseClient
|
|
128
|
+
): Promise<SchemaDriftReport> {
|
|
129
|
+
const db = customDb || getDatabase();
|
|
130
|
+
const catalog: DatabaseCatalog = generateDatabaseSchemaCatalog(rootDir);
|
|
131
|
+
const liveTables = await inspectLiveDatabase(db);
|
|
132
|
+
|
|
133
|
+
const missingTables: MissingTableInfo[] = [];
|
|
134
|
+
const missingColumns: MissingColumnInfo[] = [];
|
|
135
|
+
const orphanTables: string[] = [];
|
|
136
|
+
|
|
137
|
+
const declaredTableNames = new Set<string>();
|
|
138
|
+
|
|
139
|
+
for (const table of catalog.tables) {
|
|
140
|
+
const tName = table.name.toLowerCase();
|
|
141
|
+
declaredTableNames.add(tName);
|
|
142
|
+
|
|
143
|
+
const liveTable = liveTables.get(tName);
|
|
144
|
+
if (!liveTable) {
|
|
145
|
+
missingTables.push({
|
|
146
|
+
table: table.name,
|
|
147
|
+
sourceSlice: table.sourceSlice
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check columns
|
|
153
|
+
for (const col of table.columns) {
|
|
154
|
+
const colName = col.name.toLowerCase();
|
|
155
|
+
if (!liveTable.columns.has(colName)) {
|
|
156
|
+
missingColumns.push({
|
|
157
|
+
table: table.name,
|
|
158
|
+
column: col.name,
|
|
159
|
+
expectedType: col.sqlType
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for orphan tables
|
|
166
|
+
for (const [liveName, tbl] of liveTables.entries()) {
|
|
167
|
+
if (!declaredTableNames.has(liveName)) {
|
|
168
|
+
orphanTables.push(tbl.name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hasDrift = missingTables.length > 0 || missingColumns.length > 0 || orphanTables.length > 0;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
status: hasDrift ? 'DRIFT_DETECTED' : 'PASS',
|
|
176
|
+
totalDeclaredTables: catalog.totalTables,
|
|
177
|
+
totalLiveTables: liveTables.size,
|
|
178
|
+
drift: {
|
|
179
|
+
missingTables,
|
|
180
|
+
missingColumns,
|
|
181
|
+
orphanTables
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Slice Directory Resolution & Discovery
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for locating `src/slices`. Every consumer (CLI, MCP,
|
|
5
|
+
* runtime, migrations, scaffolder) resolves through here.
|
|
6
|
+
*
|
|
7
|
+
* Fails loudly with structured diagnostics instead of silently reporting
|
|
8
|
+
* success when no slices directory exists — the previous source of
|
|
9
|
+
* false-green reports to AI agents.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { Err, Ok, type Result } from '../core/machine-types';
|
|
15
|
+
|
|
16
|
+
export const SLICE_EXTENSION = '.slice.tsx';
|
|
17
|
+
|
|
18
|
+
export type SlicesDirErrorCode = 'NO_SLICES_DIR' | 'AMBIGUOUS_SLICES_DIR';
|
|
19
|
+
|
|
20
|
+
export interface SlicesDirError {
|
|
21
|
+
code: SlicesDirErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
candidates: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SlicesResolution {
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
slicesDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively collects every `*.slice.tsx` under `dir`.
|
|
33
|
+
* Returns an empty array when the directory does not exist.
|
|
34
|
+
*/
|
|
35
|
+
export function findSliceFiles(dir: string): string[] {
|
|
36
|
+
if (!fs.existsSync(dir)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results: string[] = [];
|
|
41
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
42
|
+
const full = path.join(dir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
results.push(...findSliceFiles(full));
|
|
45
|
+
} else if (entry.name.endsWith(SLICE_EXTENSION)) {
|
|
46
|
+
results.push(full);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readWorkspaceGlobs(projectRoot: string): string[] {
|
|
53
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
54
|
+
if (!fs.existsSync(pkgPath)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
60
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
61
|
+
return pkg.workspaces;
|
|
62
|
+
}
|
|
63
|
+
if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
|
|
64
|
+
return pkg.workspaces.packages;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function expandWorkspaceGlobs(projectRoot: string, globs: string[]): string[] {
|
|
74
|
+
const dirs: string[] = [];
|
|
75
|
+
|
|
76
|
+
for (const glob of globs) {
|
|
77
|
+
if (!glob.endsWith('/*')) {
|
|
78
|
+
dirs.push(path.resolve(projectRoot, glob));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parent = path.resolve(projectRoot, glob.slice(0, -2));
|
|
83
|
+
if (!fs.existsSync(parent)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
88
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
89
|
+
dirs.push(path.join(parent, entry.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return dirs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasSlices(dir: string): boolean {
|
|
98
|
+
return findSliceFiles(dir).length > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the slices directory starting from `startDir`.
|
|
103
|
+
*
|
|
104
|
+
* Order:
|
|
105
|
+
* 1. `SYNAPSE_ROOT` (relative to `startDir`) overrides the starting point.
|
|
106
|
+
* 2. `<projectRoot>/src/slices` when it contains at least one slice.
|
|
107
|
+
* 3. Workspace members declared in `package.json#workspaces` — only when
|
|
108
|
+
* exactly one of them owns a non-empty `src/slices`.
|
|
109
|
+
*
|
|
110
|
+
* Ambiguity and absence are both errors: a machine consumer must never be
|
|
111
|
+
* told "PASS" when nothing was discovered.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveSlicesDir(startDir: string = process.cwd()): Result<SlicesResolution, SlicesDirError> {
|
|
114
|
+
const envRoot = process.env.SYNAPSE_ROOT;
|
|
115
|
+
const projectRoot = envRoot ? path.resolve(startDir, envRoot) : path.resolve(startDir);
|
|
116
|
+
const candidates: string[] = [];
|
|
117
|
+
|
|
118
|
+
const direct = path.join(projectRoot, 'src/slices');
|
|
119
|
+
candidates.push(direct);
|
|
120
|
+
if (hasSlices(direct)) {
|
|
121
|
+
return Ok({ projectRoot, slicesDir: direct });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const discovered: string[] = [];
|
|
125
|
+
for (const workspaceDir of expandWorkspaceGlobs(projectRoot, readWorkspaceGlobs(projectRoot))) {
|
|
126
|
+
const candidate = path.join(workspaceDir, 'src/slices');
|
|
127
|
+
candidates.push(candidate);
|
|
128
|
+
if (hasSlices(candidate)) {
|
|
129
|
+
discovered.push(candidate);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (discovered.length === 1) {
|
|
134
|
+
return Ok({ projectRoot, slicesDir: discovered[0] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (discovered.length > 1) {
|
|
138
|
+
return Err({
|
|
139
|
+
code: 'AMBIGUOUS_SLICES_DIR',
|
|
140
|
+
message:
|
|
141
|
+
`Múltiplos diretórios de fatias encontrados a partir de '${projectRoot}'. ` +
|
|
142
|
+
`Defina SYNAPSE_ROOT ou execute o comando dentro do app alvo.`,
|
|
143
|
+
candidates: discovered
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Err({
|
|
148
|
+
code: 'NO_SLICES_DIR',
|
|
149
|
+
message:
|
|
150
|
+
`Nenhum diretório de fatias (src/slices contendo arquivos *${SLICE_EXTENSION}) ` +
|
|
151
|
+
`encontrado a partir de '${projectRoot}'.`,
|
|
152
|
+
candidates
|
|
153
|
+
});
|
|
154
|
+
}
|