@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,117 @@
1
+ /**
2
+ * SynapseJS - Standalone Production Build Generator
3
+ *
4
+ * Produces an ultra-optimized, self-contained deployment package under `.synapse/standalone/`
5
+ * containing pre-compiled client bundles, server-side slice execution runtime,
6
+ * static assets, and minimal production package.json for zero-dependency container deployment.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { Err, Ok, type Result } from '../core/machine-types';
12
+ import { type BuildReportEntry, buildAllClientBundles } from './client-bundler';
13
+ import { resolveSlicesDir, type SlicesDirError } from './slice-discovery';
14
+
15
+ export interface StandaloneBuildResult {
16
+ status: 'PASS' | 'FAIL';
17
+ outDir: string;
18
+ clientBundlesCount: number;
19
+ totalBytes: number;
20
+ serverEntry: string;
21
+ entries: BuildReportEntry[];
22
+ }
23
+
24
+ export async function buildStandalone(
25
+ appRoot: string
26
+ ): Promise<Result<StandaloneBuildResult, SlicesDirError | { code: string; message: string }>> {
27
+ const resolution = resolveSlicesDir(appRoot);
28
+ if (!resolution.ok) {
29
+ return Err(resolution.error);
30
+ }
31
+
32
+ // 1. Pre-build all client bundles and manifest
33
+ const clientBuild = await buildAllClientBundles(appRoot);
34
+ if (!clientBuild.ok) {
35
+ return Err(clientBuild.error);
36
+ }
37
+
38
+ const standaloneDir = path.join(appRoot, '.synapse', 'standalone');
39
+ fs.rmSync(standaloneDir, { recursive: true, force: true });
40
+ fs.mkdirSync(standaloneDir, { recursive: true });
41
+
42
+ // 2. Copy pre-built client bundles
43
+ const clientSrc = path.join(appRoot, '.synapse', 'client');
44
+ const clientDest = path.join(standaloneDir, '.synapse', 'client');
45
+ if (fs.existsSync(clientSrc)) {
46
+ fs.cpSync(clientSrc, clientDest, { recursive: true });
47
+ }
48
+
49
+ // 3. Copy public/ if present
50
+ const publicSrc = path.join(appRoot, 'public');
51
+ const publicDest = path.join(standaloneDir, 'public');
52
+ if (fs.existsSync(publicSrc)) {
53
+ fs.cpSync(publicSrc, publicDest, { recursive: true });
54
+ }
55
+
56
+ // 4. Copy slices for server-side SSR and actions
57
+ const slicesSrc = resolution.value.slicesDir;
58
+ const relSlices = path.relative(appRoot, slicesSrc);
59
+ const slicesDest = path.join(standaloneDir, relSlices);
60
+ fs.mkdirSync(path.dirname(slicesDest), { recursive: true });
61
+ fs.cpSync(slicesSrc, slicesDest, { recursive: true });
62
+
63
+ // 5. Copy package.json & tsconfig.json if present
64
+ const pkgSrc = path.join(appRoot, 'package.json');
65
+ if (fs.existsSync(pkgSrc)) {
66
+ fs.copyFileSync(pkgSrc, path.join(standaloneDir, 'package.json'));
67
+ }
68
+
69
+ const tsconfigSrc = path.join(appRoot, 'tsconfig.json');
70
+ if (fs.existsSync(tsconfigSrc)) {
71
+ fs.copyFileSync(tsconfigSrc, path.join(standaloneDir, 'tsconfig.json'));
72
+ }
73
+
74
+ // 6. Write standalone server bootstrap
75
+ const serverEntryCode = `/**
76
+ * SynapseJS Standalone Production Server
77
+ */
78
+ import { SynapseServer } from 'synapsejs';
79
+
80
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
81
+ const server = new SynapseServer(import.meta.dir, port);
82
+
83
+ console.log(\`⚡ [SynapseJS Standalone] Iniciando servidor de produção na porta \${port}...\`);
84
+ await server.discoverSlices();
85
+ await server.start();
86
+ `;
87
+
88
+ const serverEntryPath = path.join(standaloneDir, 'server.ts');
89
+ fs.writeFileSync(serverEntryPath, serverEntryCode, 'utf-8');
90
+
91
+ // Compute stats
92
+ let totalBytes = 0;
93
+ function computeDirSize(dir: string) {
94
+ if (!fs.existsSync(dir)) return;
95
+ const items = fs.readdirSync(dir, { withFileTypes: true });
96
+ for (const item of items) {
97
+ const full = path.join(dir, item.name);
98
+ if (item.isDirectory()) {
99
+ computeDirSize(full);
100
+ } else if (item.isFile()) {
101
+ totalBytes += fs.statSync(full).size;
102
+ }
103
+ }
104
+ }
105
+ computeDirSize(standaloneDir);
106
+
107
+ const passedEntries = clientBuild.value.entries.filter((e) => e.status === 'PASS');
108
+
109
+ return Ok({
110
+ status: 'PASS',
111
+ outDir: standaloneDir,
112
+ clientBundlesCount: passedEntries.length,
113
+ totalBytes,
114
+ serverEntry: serverEntryPath,
115
+ entries: clientBuild.value.entries
116
+ });
117
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * SynapseJS - Unified Action Context & Service Extensibility
3
+ *
4
+ * Provides a rich, extensible execution context for slices without breaking Locality of Behavior.
5
+ * Combines database access, authenticated session, multi-tenancy, service locator,
6
+ * structured logging, and background job dispatch.
7
+ */
8
+
9
+ import { getEventHub } from '../runtime/event-hub';
10
+ import type { DatabaseClient, QueryOptions, WhereCondition } from './database-client';
11
+ import type { SessionContext } from './session-context';
12
+ import { getStorage, type StorageClient } from './storage';
13
+
14
+ export interface StructuredLogger {
15
+ info(message: string, context?: Record<string, unknown>): void;
16
+ warn(message: string, context?: Record<string, unknown>): void;
17
+ error(message: string, error?: unknown, context?: Record<string, unknown>): void;
18
+ debug(message: string, context?: Record<string, unknown>): void;
19
+ }
20
+
21
+ export interface JobEnqueueOptions {
22
+ delaySeconds?: number;
23
+ priority?: number;
24
+ }
25
+
26
+ export type EnqueueFn = <TPayload>(
27
+ jobOrName: { name: string } | string,
28
+ payload: TPayload,
29
+ options?: JobEnqueueOptions
30
+ ) => Promise<string>;
31
+
32
+ export type BroadcastFn = (topic: string, data: unknown) => number;
33
+
34
+ export interface ActionContext<TServices = Record<string, unknown>> extends DatabaseClient {
35
+ /** Underlying Database Client */
36
+ readonly db: DatabaseClient;
37
+
38
+ /** Authenticated or anonymous session context */
39
+ readonly session: SessionContext;
40
+
41
+ /** Multi-tenant identifier (if resolved from subdomain/header/token) */
42
+ readonly tenantId?: string;
43
+
44
+ /** Registered enterprise/SaaS services (e.g. mailer, stripe, s3) */
45
+ readonly services: TServices;
46
+
47
+ /** Structured logger */
48
+ readonly logger: StructuredLogger;
49
+
50
+ /** Enqueues a background job for asynchronous processing */
51
+ readonly enqueue: EnqueueFn;
52
+
53
+ /** Unified object storage client */
54
+ readonly storage: StorageClient;
55
+
56
+ /** Broadcasts a real-time event to SSE subscribers */
57
+ readonly broadcast: BroadcastFn;
58
+
59
+ /** Lazy adapter for Kysely (if configured or requested) */
60
+ readonly kysely?: unknown;
61
+ }
62
+
63
+ export function createDefaultLogger(sliceName?: string): StructuredLogger {
64
+ const prefix = sliceName ? `[${sliceName}]` : '[SynapseJS]';
65
+ return {
66
+ info: (msg, ctx) => console.log(`${prefix} INFO: ${msg}`, ctx ? JSON.stringify(ctx) : ''),
67
+ warn: (msg, ctx) => console.warn(`${prefix} WARN: ${msg}`, ctx ? JSON.stringify(ctx) : ''),
68
+ error: (msg, err, ctx) => console.error(`${prefix} ERROR: ${msg}`, err, ctx ? JSON.stringify(ctx) : ''),
69
+ debug: (msg, ctx) => {
70
+ if (process.env.DEBUG) {
71
+ console.debug(`${prefix} DEBUG: ${msg}`, ctx ? JSON.stringify(ctx) : '');
72
+ }
73
+ }
74
+ };
75
+ }
76
+
77
+ export interface ActionContextOptions<TServices = Record<string, unknown>> {
78
+ db: DatabaseClient;
79
+ session: SessionContext;
80
+ tenantId?: string;
81
+ services?: TServices;
82
+ logger?: StructuredLogger;
83
+ enqueue?: EnqueueFn;
84
+ storage?: StorageClient;
85
+ broadcast?: BroadcastFn;
86
+ kysely?: unknown;
87
+ }
88
+
89
+ /**
90
+ * Creates an ActionContext that is also a fully functional DatabaseClient,
91
+ * providing 100% backward and forward compatibility.
92
+ */
93
+ export function createActionContext<TServices = Record<string, unknown>>(
94
+ options: ActionContextOptions<TServices>
95
+ ): ActionContext<TServices> {
96
+ const {
97
+ db,
98
+ session,
99
+ tenantId = session.tenantId,
100
+ services = {} as TServices,
101
+ logger = createDefaultLogger(),
102
+ enqueue = async () => 'job_noop',
103
+ storage = getStorage(),
104
+ broadcast = (topic: string, data: unknown) => getEventHub().publish(topic, data),
105
+ kysely
106
+ } = options;
107
+
108
+ const context: ActionContext<TServices> = {
109
+ // ActionContext properties
110
+ db,
111
+ session,
112
+ tenantId,
113
+ services,
114
+ logger,
115
+ enqueue,
116
+ storage,
117
+ broadcast,
118
+ kysely,
119
+
120
+ // DatabaseClient proxy methods
121
+ query: <T = unknown>(sql: string, params?: unknown[]) => db.query<T>(sql, params),
122
+ queryOne: <T = unknown>(sql: string, params?: unknown[]) => db.queryOne<T>(sql, params),
123
+ sql: <T = unknown>(strings: TemplateStringsArray, ...values: unknown[]) => db.sql<T>(strings, ...values),
124
+ sqlOne: <T = unknown>(strings: TemplateStringsArray, ...values: unknown[]) => db.sqlOne<T>(strings, ...values),
125
+ findMany: <T = unknown>(table: string, options?: QueryOptions) => db.findMany<T>(table, options),
126
+ findOne: <T = unknown>(table: string, options?: QueryOptions) => db.findOne<T>(table, options),
127
+ insert: <T = unknown>(table: string, data: Record<string, unknown>) => db.insert<T>(table, data),
128
+ update: <T = unknown>(table: string, data: Record<string, unknown>, where: WhereCondition) =>
129
+ db.update<T>(table, data, where),
130
+ delete: (table: string, where: WhereCondition) => db.delete(table, where),
131
+ transaction: async <T>(operation: (tx: DatabaseClient) => Promise<T>): Promise<T> => {
132
+ return db.transaction(async (txDb) => {
133
+ // Create transactional ActionContext preserving services and context
134
+ const txContext = createActionContext({
135
+ db: txDb,
136
+ session,
137
+ tenantId,
138
+ services,
139
+ logger,
140
+ enqueue,
141
+ storage,
142
+ broadcast,
143
+ kysely: txDb
144
+ });
145
+ return operation(txContext);
146
+ });
147
+ },
148
+ close: () => db.close?.()
149
+ };
150
+
151
+ return context;
152
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SynapseJS - Enterprise & SaaS Configuration Contract
3
+ *
4
+ * Provides a declarative configuration entry point via `synapse.config.ts`.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
10
+ export interface SynapsePlugin {
11
+ name: string;
12
+ // biome-ignore lint/suspicious/noExplicitAny: generic boundary for framework server
13
+ onBootstrap?: (server: any) => Promise<void> | void;
14
+ onRequest?: (req: Request) => Promise<Response | null | void> | Response | null | void;
15
+ onResponse?: (res: Response, req: Request) => Promise<Response> | Response;
16
+ // biome-ignore lint/suspicious/noExplicitAny: generic boundary for database client
17
+ onMigrate?: (db: any) => Promise<void> | void;
18
+ }
19
+
20
+ export interface SynapseConfig<TServices = Record<string, unknown>> {
21
+ /**
22
+ * Enterprise & SaaS service registry (e.g. mailer, stripe, storage, ai).
23
+ * Can be an object or an async factory returning the services.
24
+ */
25
+ services?: TServices | (() => Promise<TServices>) | (() => TServices);
26
+
27
+ /**
28
+ * Multi-tenancy configuration.
29
+ */
30
+ multitenancy?: {
31
+ strategy?: 'subdomain' | 'header' | 'cookie';
32
+ headerName?: string;
33
+ };
34
+
35
+ /**
36
+ * Background queue settings.
37
+ */
38
+ queue?: {
39
+ driver?: 'sqlite' | 'memory' | 'redis';
40
+ dbPath?: string;
41
+ concurrency?: number;
42
+ };
43
+
44
+ /**
45
+ * Infrastructure lifecycle plugins (onBootstrap, onRequest, onResponse, onMigrate).
46
+ */
47
+ plugins?: SynapsePlugin[];
48
+ }
49
+
50
+ export function defineConfig<TServices = Record<string, unknown>>(
51
+ config: SynapseConfig<TServices>
52
+ ): SynapseConfig<TServices> {
53
+ return config;
54
+ }
55
+
56
+ const CONFIG_FILENAMES = ['synapse.config.ts', 'synapse.config.js', 'synapse.config.mjs'];
57
+
58
+ /**
59
+ * Discovers and loads `synapse.config.(ts|js|mjs)` from the application root.
60
+ */
61
+ export async function loadSynapseConfig(appDir: string): Promise<SynapseConfig> {
62
+ for (const filename of CONFIG_FILENAMES) {
63
+ const candidate = path.join(appDir, filename);
64
+ if (fs.existsSync(candidate)) {
65
+ try {
66
+ const mod = await import(candidate);
67
+ const config = mod.default || mod;
68
+ return typeof config === 'function' ? await config() : config;
69
+ } catch (err: unknown) {
70
+ const errMessage = err instanceof Error ? err.message : String(err);
71
+ console.warn(`[SynapseConfig] Aviso ao carregar ${filename}: ${errMessage}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ return {};
77
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * SynapseJS - Database Context & Query Builder Abstraction
3
+ *
4
+ * Provides a clean, type-safe persistence contract without reflection-based ORM bloat.
5
+ * Compatible with raw parameterized SQL, Kysely, and Drizzle query builders.
6
+ */
7
+
8
+ export interface QueryResult<T = unknown> {
9
+ rows: T[];
10
+ rowCount: number;
11
+ }
12
+
13
+ export function compileTaggedSql(
14
+ strings: TemplateStringsArray,
15
+ ...values: unknown[]
16
+ ): { text: string; params: unknown[] } {
17
+ let text = '';
18
+ const params: unknown[] = [];
19
+
20
+ for (let i = 0; i < strings.length; i++) {
21
+ text += strings[i];
22
+ if (i < values.length) {
23
+ params.push(values[i]);
24
+ text += `$${params.length}`;
25
+ }
26
+ }
27
+
28
+ return { text, params };
29
+ }
30
+
31
+ import {
32
+ compileDelete,
33
+ compileInsert,
34
+ compileSelect,
35
+ compileUpdate,
36
+ type JoinClause,
37
+ nestJoinedRow,
38
+ type QueryOptions,
39
+ type WhereCondition,
40
+ type WhereOperator
41
+ } from './query-builder';
42
+
43
+ export {
44
+ compileDelete,
45
+ compileInsert,
46
+ compileSelect,
47
+ compileUpdate,
48
+ type JoinClause,
49
+ nestJoinedRow,
50
+ type QueryOptions,
51
+ type WhereCondition,
52
+ type WhereOperator
53
+ };
54
+
55
+ export interface DatabaseClient {
56
+ /**
57
+ * Execute parameterized SQL query with explicit generic return type.
58
+ */
59
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
60
+
61
+ /**
62
+ * Execute single-row query, returning null if not found.
63
+ */
64
+ queryOne<T = unknown>(sql: string, params?: unknown[]): Promise<T | null>;
65
+
66
+ /**
67
+ * Execute Tagged Template Literal SQL query with auto-parameterization.
68
+ */
69
+ sql<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
70
+
71
+ /**
72
+ * Execute Tagged Template Literal SQL query returning first row or null.
73
+ */
74
+ sqlOne<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null>;
75
+
76
+ /**
77
+ * Declarative type-safe SELECT with parameterized filters, ordering and pagination.
78
+ */
79
+ findMany<T = unknown>(table: string, options?: QueryOptions): Promise<T[]>;
80
+
81
+ /**
82
+ * Declarative type-safe single-row SELECT, returning null if not found.
83
+ */
84
+ findOne<T = unknown>(table: string, options?: QueryOptions): Promise<T | null>;
85
+
86
+ /**
87
+ * Declarative type-safe INSERT returning the created row.
88
+ */
89
+ insert<T = unknown>(table: string, data: Record<string, unknown>): Promise<T>;
90
+
91
+ /**
92
+ * Declarative type-safe UPDATE with WHERE condition, returning modified rows.
93
+ */
94
+ update<T = unknown>(table: string, data: Record<string, unknown>, where: WhereCondition): Promise<T[]>;
95
+
96
+ /**
97
+ * Declarative type-safe DELETE with WHERE condition, returning number of affected rows.
98
+ */
99
+ delete(table: string, where: WhereCondition): Promise<number>;
100
+
101
+ /**
102
+ * Execute command inside a database transaction boundary.
103
+ */
104
+ transaction<T>(operation: (tx: DatabaseClient) => Promise<T>): Promise<T>;
105
+
106
+ /**
107
+ * Closes active connections or statement pools.
108
+ */
109
+ close?(): void | Promise<void>;
110
+ }
111
+
112
+ /**
113
+ * In-memory Mock Database Client for deterministic testing and PBT (Fast-Check).
114
+ */
115
+ export class MockDatabaseClient implements DatabaseClient {
116
+ private tables: Map<string, Array<Record<string, unknown>>> = new Map();
117
+ private queryHandlers: Array<{
118
+ match: RegExp | string;
119
+ handler: (params: unknown[]) => unknown[];
120
+ }> = [];
121
+
122
+ /** Every statement the client was asked to execute, in order. */
123
+ readonly calls: Array<{ sql: string; params: unknown[] }> = [];
124
+
125
+ constructor(initialData?: Record<string, Array<Record<string, unknown>>>) {
126
+ if (initialData) {
127
+ for (const [table, rows] of Object.entries(initialData)) {
128
+ this.tables.set(table, [...rows]);
129
+ }
130
+ }
131
+ }
132
+
133
+ onQuery(pattern: RegExp | string, handler: (params: unknown[]) => unknown[]): this {
134
+ this.queryHandlers.push({ match: pattern, handler });
135
+ return this;
136
+ }
137
+
138
+ async query<T = unknown>(sql: string, params: unknown[] = []): Promise<T[]> {
139
+ this.calls.push({ sql, params });
140
+
141
+ for (const { match, handler } of this.queryHandlers) {
142
+ const isMatch = typeof match === 'string' ? sql.includes(match) : match.test(sql);
143
+ if (isMatch) {
144
+ return handler(params) as T[];
145
+ }
146
+ }
147
+
148
+ // Default mock response: check if table matches initialData for SELECT queries
149
+ if (/^\s*SELECT\b/i.test(sql)) {
150
+ for (const [table, rows] of this.tables.entries()) {
151
+ if (sql.includes(table)) {
152
+ return [...rows] as unknown as T[];
153
+ }
154
+ }
155
+ }
156
+
157
+ // Default mock response: empty set
158
+ return [] as T[];
159
+ }
160
+
161
+ async queryOne<T = unknown>(sql: string, params: unknown[] = []): Promise<T | null> {
162
+ const results = await this.query<T>(sql, params);
163
+ return results.length > 0 ? results[0] : null;
164
+ }
165
+
166
+ async sql<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]> {
167
+ const { text, params } = compileTaggedSql(strings, ...values);
168
+ return this.query<T>(text, params);
169
+ }
170
+
171
+ async sqlOne<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T | null> {
172
+ const { text, params } = compileTaggedSql(strings, ...values);
173
+ return this.queryOne<T>(text, params);
174
+ }
175
+
176
+ async findMany<T = unknown>(table: string, options?: QueryOptions): Promise<T[]> {
177
+ const { sql, params } = compileSelect(table, options);
178
+ const results = await this.query<Record<string, unknown>>(sql, params);
179
+ let rows: Array<Record<string, unknown>> = results;
180
+
181
+ if (rows.length === 0) {
182
+ const inMemory = this.tables.get(table);
183
+ if (inMemory) {
184
+ rows = inMemory;
185
+ }
186
+ }
187
+
188
+ if (options?.join && options.join.length > 0) {
189
+ return rows.map((r) => nestJoinedRow<T>(r, options.join));
190
+ }
191
+ return rows as unknown as T[];
192
+ }
193
+
194
+ async findOne<T = unknown>(table: string, options?: QueryOptions): Promise<T | null> {
195
+ const opts = { ...options, limit: 1 };
196
+ const { sql, params } = compileSelect(table, opts);
197
+ const result = await this.queryOne<Record<string, unknown>>(sql, params);
198
+ let row: Record<string, unknown> | null = result;
199
+
200
+ if (row === null) {
201
+ const inMemory = this.tables.get(table);
202
+ if (inMemory && inMemory.length > 0) {
203
+ row = inMemory[0];
204
+ }
205
+ }
206
+
207
+ if (!row) return null;
208
+ if (options?.join && options.join.length > 0) {
209
+ return nestJoinedRow<T>(row, options.join);
210
+ }
211
+ return row as unknown as T;
212
+ }
213
+
214
+ async insert<T = unknown>(table: string, data: Record<string, unknown>): Promise<T> {
215
+ const { sql, params } = compileInsert(table, data);
216
+ const results = await this.query<T>(sql, params);
217
+ if (results.length > 0) {
218
+ return results[0];
219
+ }
220
+
221
+ const tableRows = this.tables.get(table) || [];
222
+ const created = { ...data };
223
+ tableRows.push(created);
224
+ this.tables.set(table, tableRows);
225
+ return created as T;
226
+ }
227
+
228
+ async update<T = unknown>(table: string, data: Record<string, unknown>, where: WhereCondition): Promise<T[]> {
229
+ const { sql, params } = compileUpdate(table, data, where);
230
+ const results = await this.query<T>(sql, params);
231
+ if (results.length > 0) return results;
232
+ return [data as T];
233
+ }
234
+
235
+ async delete(table: string, where: WhereCondition): Promise<number> {
236
+ const { sql, params } = compileDelete(table, where);
237
+ const results = await this.query(sql, params);
238
+ return results.length > 0 ? results.length : 1;
239
+ }
240
+
241
+ async transaction<T>(operation: (tx: DatabaseClient) => Promise<T>): Promise<T> {
242
+ return operation(this);
243
+ }
244
+
245
+ close(): void {}
246
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SynapseJS - Universal Database Factory
3
+ *
4
+ * Auto-detects the database engine from DATABASE_URL:
5
+ * - If postgres:// or postgresql:// -> Instantiates PostgresDatabaseClient
6
+ * - Otherwise -> Instantiates embedded SqliteDatabaseClient
7
+ */
8
+
9
+ import * as path from 'path';
10
+ import type { DatabaseClient } from './database-client';
11
+ import { PostgresDatabaseClient } from './postgres-client';
12
+ import { SqliteDatabaseClient } from './sqlite-client';
13
+
14
+ let cachedInstance: DatabaseClient | null = null;
15
+
16
+ export function getDatabase(connectionUri?: string): DatabaseClient {
17
+ if (cachedInstance) {
18
+ return cachedInstance;
19
+ }
20
+
21
+ const uri = connectionUri || process.env.DATABASE_URL;
22
+
23
+ if (uri && (uri.startsWith('postgres://') || uri.startsWith('postgresql://'))) {
24
+ cachedInstance = new PostgresDatabaseClient(uri);
25
+ return cachedInstance;
26
+ }
27
+
28
+ // A configured engine that is neither supported nor a file path must not be
29
+ // silently downgraded to embedded SQLite: that would make a Postgres parity
30
+ // check pass against the wrong engine.
31
+ if (uri && /^[a-z][a-z0-9+.-]*:\/\//i.test(uri)) {
32
+ throw new Error(
33
+ `DATABASE_URL '${uri}' usa um esquema não suportado. Use postgres://, postgresql:// ou um caminho de arquivo SQLite.`
34
+ );
35
+ }
36
+
37
+ // Fallback to embedded SQLite
38
+ const dbPath = uri || path.join(process.cwd(), '.synapse/synapse.sqlite');
39
+ cachedInstance = new SqliteDatabaseClient(dbPath);
40
+
41
+ return cachedInstance;
42
+ }
43
+
44
+ export function resetDatabaseInstance(): void {
45
+ cachedInstance = null;
46
+ }