@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,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Scaffolder Fields Parser & Code Generator
|
|
3
|
+
*
|
|
4
|
+
* Parses `--fields="name:string,email:string,status:enum(ACTIVE|INACTIVE),price:number"`
|
|
5
|
+
* and compiles into TypeBox definitions, SQL DDL column statements,
|
|
6
|
+
* DataForm/DataTable specs, and Fast-Check PBT test arbitraries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type FieldType = 'string' | 'number' | 'integer' | 'boolean' | 'enum' | 'date' | 'timestamp' | 'json';
|
|
10
|
+
|
|
11
|
+
export interface ParsedField {
|
|
12
|
+
name: string;
|
|
13
|
+
type: FieldType;
|
|
14
|
+
required: boolean;
|
|
15
|
+
enumValues?: string[];
|
|
16
|
+
format?: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fieldToLabel(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.replace(/_/g, ' ')
|
|
23
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
24
|
+
.replace(/^./, (s) => s.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses a comma-separated field specification string.
|
|
29
|
+
* Example: "name:string,email:string,status:enum(PENDING|ACTIVE|CANCELLED),price:number,notes:string?"
|
|
30
|
+
*/
|
|
31
|
+
export function parseFields(fieldsStr?: string): ParsedField[] {
|
|
32
|
+
if (!fieldsStr?.trim()) {
|
|
33
|
+
return [
|
|
34
|
+
{ name: 'name', type: 'string', required: true, label: 'Name' },
|
|
35
|
+
{ name: 'email', type: 'string', format: 'email', required: true, label: 'Email' }
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rawFields = fieldsStr
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((f) => f.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
const fields: ParsedField[] = [];
|
|
44
|
+
|
|
45
|
+
for (const raw of rawFields) {
|
|
46
|
+
let [namePart, typePart = 'string'] = raw.split(':').map((s) => s.trim());
|
|
47
|
+
let required = true;
|
|
48
|
+
|
|
49
|
+
if (namePart.endsWith('?')) {
|
|
50
|
+
namePart = namePart.slice(0, -1);
|
|
51
|
+
required = false;
|
|
52
|
+
} else if (typePart.endsWith('?')) {
|
|
53
|
+
typePart = typePart.slice(0, -1);
|
|
54
|
+
required = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let type: FieldType = 'string';
|
|
58
|
+
let enumValues: string[] | undefined;
|
|
59
|
+
let format: string | undefined;
|
|
60
|
+
|
|
61
|
+
const lowerType = typePart.toLowerCase();
|
|
62
|
+
|
|
63
|
+
if (lowerType.startsWith('enum(') && lowerType.endsWith(')')) {
|
|
64
|
+
type = 'enum';
|
|
65
|
+
const inner = typePart.slice(5, -1);
|
|
66
|
+
enumValues = inner
|
|
67
|
+
.split('|')
|
|
68
|
+
.map((v) => v.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
if (enumValues.length === 0) {
|
|
71
|
+
enumValues = ['DEFAULT'];
|
|
72
|
+
}
|
|
73
|
+
} else if (lowerType === 'number' || lowerType === 'float' || lowerType === 'double') {
|
|
74
|
+
type = 'number';
|
|
75
|
+
} else if (lowerType === 'int' || lowerType === 'integer') {
|
|
76
|
+
type = 'integer';
|
|
77
|
+
} else if (lowerType === 'bool' || lowerType === 'boolean') {
|
|
78
|
+
type = 'boolean';
|
|
79
|
+
} else if (lowerType === 'date') {
|
|
80
|
+
type = 'date';
|
|
81
|
+
format = 'date';
|
|
82
|
+
} else if (lowerType === 'timestamp' || lowerType === 'datetime') {
|
|
83
|
+
type = 'timestamp';
|
|
84
|
+
} else if (lowerType === 'json' || lowerType === 'object') {
|
|
85
|
+
type = 'json';
|
|
86
|
+
} else {
|
|
87
|
+
type = 'string';
|
|
88
|
+
if (namePart.toLowerCase().includes('email')) {
|
|
89
|
+
format = 'email';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fields.push({
|
|
94
|
+
name: namePart,
|
|
95
|
+
type,
|
|
96
|
+
required,
|
|
97
|
+
enumValues,
|
|
98
|
+
format,
|
|
99
|
+
label: fieldToLabel(namePart)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return fields;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Emits TypeBox schema definitions for the parsed fields.
|
|
108
|
+
*/
|
|
109
|
+
export function generateTypeBoxProperties(fields: ParsedField[]): string {
|
|
110
|
+
const lines = fields.map((f) => {
|
|
111
|
+
let tbType = '';
|
|
112
|
+
switch (f.type) {
|
|
113
|
+
case 'string':
|
|
114
|
+
if (f.format === 'email') {
|
|
115
|
+
tbType = `Type.String({ format: 'email' })`;
|
|
116
|
+
} else {
|
|
117
|
+
tbType = `Type.String({ minLength: 1, maxLength: 255 })`;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case 'number':
|
|
121
|
+
tbType = `Type.Number()`;
|
|
122
|
+
break;
|
|
123
|
+
case 'integer':
|
|
124
|
+
tbType = `Type.Integer()`;
|
|
125
|
+
break;
|
|
126
|
+
case 'boolean':
|
|
127
|
+
tbType = `Type.Boolean()`;
|
|
128
|
+
break;
|
|
129
|
+
case 'enum': {
|
|
130
|
+
const literals = (f.enumValues || ['DEFAULT']).map((v) => `Type.Literal('${v}')`).join(', ');
|
|
131
|
+
tbType = `Type.Union([${literals}])`;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'date':
|
|
135
|
+
case 'timestamp':
|
|
136
|
+
tbType = `Type.String()`;
|
|
137
|
+
break;
|
|
138
|
+
case 'json':
|
|
139
|
+
tbType = `Type.Record(Type.String(), Type.Any())`;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!f.required) {
|
|
144
|
+
tbType = `Type.Optional(${tbType})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return ` ${f.name}: ${tbType}`;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return lines.join(',\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Emits SQL DDL column declarations for the parsed fields.
|
|
155
|
+
*/
|
|
156
|
+
export function generateSqlColumns(fields: ParsedField[]): string {
|
|
157
|
+
const lines = fields.map((f) => {
|
|
158
|
+
let sqlType = 'TEXT';
|
|
159
|
+
switch (f.type) {
|
|
160
|
+
case 'number':
|
|
161
|
+
sqlType = 'REAL';
|
|
162
|
+
break;
|
|
163
|
+
case 'integer':
|
|
164
|
+
sqlType = 'INTEGER';
|
|
165
|
+
break;
|
|
166
|
+
case 'boolean':
|
|
167
|
+
sqlType = 'BOOLEAN DEFAULT FALSE';
|
|
168
|
+
break;
|
|
169
|
+
case 'date':
|
|
170
|
+
case 'timestamp':
|
|
171
|
+
sqlType = 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP';
|
|
172
|
+
break;
|
|
173
|
+
case 'json':
|
|
174
|
+
case 'string':
|
|
175
|
+
case 'enum':
|
|
176
|
+
default:
|
|
177
|
+
sqlType = 'TEXT';
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const nullability = f.required && f.type !== 'boolean' && f.type !== 'timestamp' ? ' NOT NULL' : '';
|
|
182
|
+
return ` ${f.name} ${sqlType}${nullability}`;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return lines.join(',\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Emits Fast-Check PBT test arbitraries for the parsed fields.
|
|
190
|
+
*/
|
|
191
|
+
export function generateFastCheckArbitraries(fields: ParsedField[]): string {
|
|
192
|
+
const arbs = fields.map((f) => {
|
|
193
|
+
let arb = '';
|
|
194
|
+
switch (f.type) {
|
|
195
|
+
case 'string':
|
|
196
|
+
if (f.format === 'email') {
|
|
197
|
+
arb = `fc.stringMatching(/^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$/)`;
|
|
198
|
+
} else {
|
|
199
|
+
arb = `fc.string({ minLength: 2, maxLength: 50 })`;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case 'number':
|
|
203
|
+
arb = `fc.double({ noNaN: true, noDefaultInfinity: true, min: 0, max: 10000 })`;
|
|
204
|
+
break;
|
|
205
|
+
case 'integer':
|
|
206
|
+
arb = `fc.integer({ min: 1, max: 100000 })`;
|
|
207
|
+
break;
|
|
208
|
+
case 'boolean':
|
|
209
|
+
arb = `fc.boolean()`;
|
|
210
|
+
break;
|
|
211
|
+
case 'enum': {
|
|
212
|
+
const vals = (f.enumValues || ['DEFAULT']).map((v) => `'${v}'`).join(', ');
|
|
213
|
+
arb = `fc.constantFrom(${vals})`;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'date':
|
|
217
|
+
case 'timestamp':
|
|
218
|
+
arb = `fc.date().map((d) => d.toISOString())`;
|
|
219
|
+
break;
|
|
220
|
+
case 'json':
|
|
221
|
+
arb = `fc.constant({})`;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return arb;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return arbs.join(',\n ');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Emits FormField array code for the React <DataForm> component.
|
|
233
|
+
*/
|
|
234
|
+
export function generateFormFieldsCode(fields: ParsedField[]): string {
|
|
235
|
+
const items = fields.map((f) => {
|
|
236
|
+
let formType = 'text';
|
|
237
|
+
let extra = '';
|
|
238
|
+
|
|
239
|
+
if (f.type === 'number' || f.type === 'integer') {
|
|
240
|
+
formType = 'number';
|
|
241
|
+
} else if (f.type === 'boolean') {
|
|
242
|
+
formType = 'checkbox';
|
|
243
|
+
} else if (f.type === 'date') {
|
|
244
|
+
formType = 'date';
|
|
245
|
+
} else if (f.type === 'enum') {
|
|
246
|
+
formType = 'select';
|
|
247
|
+
const opts = (f.enumValues || ['DEFAULT']).map((v) => `{ label: '${v}', value: '${v}' }`).join(', ');
|
|
248
|
+
extra = `, options: [${opts}]`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return ` { name: '${f.name}', label: '${f.label}', type: '${formType}', required: ${f.required}${extra} }`;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return `[\n${items.join(',\n')}\n]`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Emits DataTableColumn array code for the React <DataTable> component.
|
|
259
|
+
*/
|
|
260
|
+
export function generateTableColumnsCode(fields: ParsedField[]): string {
|
|
261
|
+
const items = [
|
|
262
|
+
` { key: 'id', header: 'ID', sortable: true }`,
|
|
263
|
+
...fields.map((f) => ` { key: '${f.name}', header: '${f.label}', sortable: true }`)
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
return `[\n${items.join(',\n')}\n]`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - AST Diff & Cross-Slice Impact Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Discovers cross-slice dependencies (Foreign Keys, shared module imports,
|
|
5
|
+
* and table references) to calculate the precise blast radius of a change.
|
|
6
|
+
* Allows AI agents to verify only what is affected without running blind full rebuilds.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as ts from 'typescript';
|
|
12
|
+
import { extractSliceSchema } from './migration-runner';
|
|
13
|
+
import { parseTableDependencies } from './schema-dag';
|
|
14
|
+
import { findSliceFiles, resolveSlicesDir } from './slice-discovery';
|
|
15
|
+
|
|
16
|
+
export type ImpactReason = 'DIRECT' | 'FOREIGN_KEY_DEPENDENCY' | 'TABLE_REFERENCE' | 'SHARED_MODULE_IMPORT';
|
|
17
|
+
|
|
18
|
+
export interface ImpactedSlice {
|
|
19
|
+
sliceName: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
reason: ImpactReason;
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImpactAnalysisReport {
|
|
27
|
+
status: 'PASS';
|
|
28
|
+
operation: 'IMPACT_ANALYSIS';
|
|
29
|
+
target: string;
|
|
30
|
+
targetType: 'slice' | 'table' | 'shared_module';
|
|
31
|
+
totalImpacted: number;
|
|
32
|
+
impactedSlices: ImpactedSlice[];
|
|
33
|
+
recommendedCommands: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SliceAnalysisData {
|
|
37
|
+
sliceName: string;
|
|
38
|
+
domain: string;
|
|
39
|
+
filePath: string;
|
|
40
|
+
tablesCreated: string[];
|
|
41
|
+
tablesReferenced: string[];
|
|
42
|
+
sharedImports: string[];
|
|
43
|
+
queriedTables: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractQueriedTables(sourceFile: ts.SourceFile): string[] {
|
|
47
|
+
const tables = new Set<string>();
|
|
48
|
+
|
|
49
|
+
function visit(node: ts.Node) {
|
|
50
|
+
// Match db.findMany('table_name', ...), db.insert('table_name', ...) etc.
|
|
51
|
+
if (ts.isCallExpression(node)) {
|
|
52
|
+
const expr = node.expression;
|
|
53
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
54
|
+
const method = expr.name.text;
|
|
55
|
+
if (['findMany', 'findOne', 'insert', 'update', 'delete'].includes(method)) {
|
|
56
|
+
const firstArg = node.arguments[0];
|
|
57
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
58
|
+
tables.add(firstArg.text.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Match raw SQL strings e.g. FROM table_name, JOIN table_name, INTO table_name
|
|
65
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
66
|
+
const text = node.text;
|
|
67
|
+
const sqlMatches = text.matchAll(/\b(?:FROM|JOIN|INTO|UPDATE)\s+["'`]?([a-zA-Z0-9_]+)["'`]?/gi);
|
|
68
|
+
for (const m of sqlMatches) {
|
|
69
|
+
const tName = m[1].toLowerCase();
|
|
70
|
+
if (!['select', 'where', 'set', 'values'].includes(tName)) {
|
|
71
|
+
tables.add(tName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ts.forEachChild(node, visit);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visit(sourceFile);
|
|
80
|
+
return Array.from(tables);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractSharedImports(sourceFile: ts.SourceFile): string[] {
|
|
84
|
+
const imports: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const statement of sourceFile.statements) {
|
|
87
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
88
|
+
const modulePath = statement.moduleSpecifier.text;
|
|
89
|
+
if (modulePath.includes('/shared/') || modulePath.startsWith('@shared/')) {
|
|
90
|
+
imports.push(path.basename(modulePath, path.extname(modulePath)));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return imports;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Analyzes the impact of a file change, slice modification, or table mutation.
|
|
100
|
+
*/
|
|
101
|
+
export function analyzeImpact(target: string, rootDir: string = process.cwd()): ImpactAnalysisReport {
|
|
102
|
+
const resolution = resolveSlicesDir(rootDir);
|
|
103
|
+
const slicesDir = resolution.ok ? resolution.value.slicesDir : path.join(rootDir, 'src/slices');
|
|
104
|
+
const sliceFiles = fs.existsSync(slicesDir) ? findSliceFiles(slicesDir) : [];
|
|
105
|
+
|
|
106
|
+
const sliceDataList: SliceAnalysisData[] = [];
|
|
107
|
+
|
|
108
|
+
for (const filePath of sliceFiles) {
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
111
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
112
|
+
const sliceName = path.basename(filePath, '.slice.tsx');
|
|
113
|
+
const domain = path.basename(path.dirname(filePath));
|
|
114
|
+
const ddl = extractSliceSchema(sourceFile);
|
|
115
|
+
const deps = ddl ? parseTableDependencies(ddl) : { created: [], referenced: [] };
|
|
116
|
+
const sharedImports = extractSharedImports(sourceFile);
|
|
117
|
+
const queriedTables = extractQueriedTables(sourceFile);
|
|
118
|
+
|
|
119
|
+
sliceDataList.push({
|
|
120
|
+
sliceName,
|
|
121
|
+
domain,
|
|
122
|
+
filePath: path.relative(rootDir, filePath),
|
|
123
|
+
tablesCreated: deps.created,
|
|
124
|
+
tablesReferenced: deps.referenced,
|
|
125
|
+
sharedImports,
|
|
126
|
+
queriedTables
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore unparseable files
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cleanTarget = target.trim();
|
|
134
|
+
const cleanBase = path.basename(cleanTarget, '.slice.tsx');
|
|
135
|
+
|
|
136
|
+
// Identify target type
|
|
137
|
+
const isSliceTarget =
|
|
138
|
+
cleanTarget.endsWith('.slice.tsx') ||
|
|
139
|
+
sliceDataList.some(
|
|
140
|
+
(s) =>
|
|
141
|
+
s.sliceName.toLowerCase() === cleanBase.toLowerCase() ||
|
|
142
|
+
s.filePath === cleanTarget ||
|
|
143
|
+
s.filePath.endsWith(`/${cleanTarget}`)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const isSharedTarget =
|
|
147
|
+
cleanTarget.includes('shared/') ||
|
|
148
|
+
cleanTarget.includes('@shared') ||
|
|
149
|
+
(!isSliceTarget && cleanTarget.endsWith('.ts'));
|
|
150
|
+
|
|
151
|
+
const targetType: 'slice' | 'table' | 'shared_module' = isSliceTarget
|
|
152
|
+
? 'slice'
|
|
153
|
+
: isSharedTarget
|
|
154
|
+
? 'shared_module'
|
|
155
|
+
: 'table';
|
|
156
|
+
|
|
157
|
+
const impactedMap = new Map<string, ImpactedSlice>();
|
|
158
|
+
|
|
159
|
+
if (targetType === 'slice') {
|
|
160
|
+
const targetSlice = sliceDataList.find(
|
|
161
|
+
(s) =>
|
|
162
|
+
s.sliceName.toLowerCase() === cleanBase.toLowerCase() ||
|
|
163
|
+
s.filePath === cleanTarget ||
|
|
164
|
+
s.filePath.endsWith(`/${cleanTarget}`)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (targetSlice) {
|
|
168
|
+
impactedMap.set(targetSlice.sliceName, {
|
|
169
|
+
sliceName: targetSlice.sliceName,
|
|
170
|
+
domain: targetSlice.domain,
|
|
171
|
+
filePath: targetSlice.filePath,
|
|
172
|
+
reason: 'DIRECT',
|
|
173
|
+
detail: 'Fatia alvo diretamente modificada'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Find dependent slices
|
|
177
|
+
const ownedTables = new Set(targetSlice.tablesCreated);
|
|
178
|
+
|
|
179
|
+
for (const other of sliceDataList) {
|
|
180
|
+
if (other.sliceName === targetSlice.sliceName) continue;
|
|
181
|
+
|
|
182
|
+
// Foreign Key dependencies
|
|
183
|
+
const fkMatch = other.tablesReferenced.find((t) => ownedTables.has(t));
|
|
184
|
+
if (fkMatch) {
|
|
185
|
+
impactedMap.set(other.sliceName, {
|
|
186
|
+
sliceName: other.sliceName,
|
|
187
|
+
domain: other.domain,
|
|
188
|
+
filePath: other.filePath,
|
|
189
|
+
reason: 'FOREIGN_KEY_DEPENDENCY',
|
|
190
|
+
detail: `Tabela '${fkMatch}' referenciada via chave estrangeira`
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Query references
|
|
196
|
+
const queryMatch = other.queriedTables.find((t) => ownedTables.has(t));
|
|
197
|
+
if (queryMatch) {
|
|
198
|
+
impactedMap.set(other.sliceName, {
|
|
199
|
+
sliceName: other.sliceName,
|
|
200
|
+
domain: other.domain,
|
|
201
|
+
filePath: other.filePath,
|
|
202
|
+
reason: 'TABLE_REFERENCE',
|
|
203
|
+
detail: `Consulta SQL referencia tabela '${queryMatch}' da fatia alvo`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (targetType === 'shared_module') {
|
|
209
|
+
const sharedName = path.basename(cleanTarget, path.extname(cleanTarget));
|
|
210
|
+
|
|
211
|
+
for (const slice of sliceDataList) {
|
|
212
|
+
if (slice.sharedImports.includes(sharedName)) {
|
|
213
|
+
impactedMap.set(slice.sliceName, {
|
|
214
|
+
sliceName: slice.sliceName,
|
|
215
|
+
domain: slice.domain,
|
|
216
|
+
filePath: slice.filePath,
|
|
217
|
+
reason: 'SHARED_MODULE_IMPORT',
|
|
218
|
+
detail: `Importa módulo compartilhado '${sharedName}'`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// targetType === 'table'
|
|
224
|
+
const tableName = cleanTarget.toLowerCase();
|
|
225
|
+
|
|
226
|
+
for (const slice of sliceDataList) {
|
|
227
|
+
if (slice.tablesCreated.includes(tableName)) {
|
|
228
|
+
impactedMap.set(slice.sliceName, {
|
|
229
|
+
sliceName: slice.sliceName,
|
|
230
|
+
domain: slice.domain,
|
|
231
|
+
filePath: slice.filePath,
|
|
232
|
+
reason: 'DIRECT',
|
|
233
|
+
detail: `Fatia é dona da tabela '${tableName}' (CREATE TABLE)`
|
|
234
|
+
});
|
|
235
|
+
} else if (slice.tablesReferenced.includes(tableName)) {
|
|
236
|
+
impactedMap.set(slice.sliceName, {
|
|
237
|
+
sliceName: slice.sliceName,
|
|
238
|
+
domain: slice.domain,
|
|
239
|
+
filePath: slice.filePath,
|
|
240
|
+
reason: 'FOREIGN_KEY_DEPENDENCY',
|
|
241
|
+
detail: `Chave estrangeira referencia tabela '${tableName}'`
|
|
242
|
+
});
|
|
243
|
+
} else if (slice.queriedTables.includes(tableName)) {
|
|
244
|
+
impactedMap.set(slice.sliceName, {
|
|
245
|
+
sliceName: slice.sliceName,
|
|
246
|
+
domain: slice.domain,
|
|
247
|
+
filePath: slice.filePath,
|
|
248
|
+
reason: 'TABLE_REFERENCE',
|
|
249
|
+
detail: `Consulta tabela '${tableName}' via query builder ou SQL`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const impactedSlices = Array.from(impactedMap.values());
|
|
256
|
+
|
|
257
|
+
const recommendedCommands = ['synapse check', 'synapse split'];
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
targetType === 'table' ||
|
|
261
|
+
(targetType === 'slice' && impactedSlices.some((s) => s.reason === 'FOREIGN_KEY_DEPENDENCY'))
|
|
262
|
+
) {
|
|
263
|
+
recommendedCommands.push('synapse db-drift', 'synapse migrate');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (impactedSlices.length > 0) {
|
|
267
|
+
recommendedCommands.push('synapse test');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
status: 'PASS',
|
|
272
|
+
operation: 'IMPACT_ANALYSIS',
|
|
273
|
+
target: cleanTarget,
|
|
274
|
+
targetType,
|
|
275
|
+
totalImpacted: impactedSlices.length,
|
|
276
|
+
impactedSlices,
|
|
277
|
+
recommendedCommands
|
|
278
|
+
};
|
|
279
|
+
}
|