@noormdev/sdk 1.0.0-alpha.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/dist/chunk-MERTHCAJ.js +299 -0
- package/dist/chunk-MERTHCAJ.js.map +1 -0
- package/dist/engine-B4JH5RQJ.js +3 -0
- package/dist/engine-B4JH5RQJ.js.map +1 -0
- package/dist/index.d.ts +2083 -0
- package/dist/index.js +8150 -0
- package/dist/index.js.map +1 -0
- package/dist/mssql-T4M7OGEC.js +46 -0
- package/dist/mssql-T4M7OGEC.js.map +1 -0
- package/dist/mysql-C2DR4TF7.js +28 -0
- package/dist/mysql-C2DR4TF7.js.map +1 -0
- package/dist/postgres-SIDJBSIT.js +29 -0
- package/dist/postgres-SIDJBSIT.js.map +1 -0
- package/dist/sqlite-5VZTBN5J.js +21 -0
- package/dist/sqlite-5VZTBN5J.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'fs/promises';
|
|
2
|
+
import { Eta } from 'eta';
|
|
3
|
+
import { ObserverEngine } from '@logosdx/observer';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { attempt } from '@logosdx/utils';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
import JSON5 from 'json5';
|
|
8
|
+
import { parse as parse$1 } from 'yaml';
|
|
9
|
+
import { parse } from 'csv-parse/sync';
|
|
10
|
+
import v from 'voca';
|
|
11
|
+
|
|
12
|
+
// src/core/template/engine.ts
|
|
13
|
+
var observer = new ObserverEngine({
|
|
14
|
+
name: "noorm",
|
|
15
|
+
spy: process.env["NOORM_DEBUG"] ? (action) => console.error(`[noorm:${action.fn}] ${String(action.event)}`) : void 0
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// src/core/template/types.ts
|
|
19
|
+
var TEMPLATE_EXTENSION = ".tmpl";
|
|
20
|
+
var HELPER_FILENAME = "$helpers";
|
|
21
|
+
var HELPER_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
22
|
+
async function loadJs(filepath) {
|
|
23
|
+
const url = pathToFileURL(filepath).href;
|
|
24
|
+
const urlWithCacheBust = `${url}?t=${Date.now()}`;
|
|
25
|
+
const mod = await import(urlWithCacheBust);
|
|
26
|
+
return mod.default !== void 0 ? mod.default : mod;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/core/template/helpers.ts
|
|
30
|
+
async function findHelperFiles(fromDir, projectRoot) {
|
|
31
|
+
const helperPaths = [];
|
|
32
|
+
let currentDir = path.resolve(fromDir);
|
|
33
|
+
const root = path.resolve(projectRoot);
|
|
34
|
+
while (currentDir.startsWith(root)) {
|
|
35
|
+
const helperPath = await findHelperInDir(currentDir);
|
|
36
|
+
if (helperPath) {
|
|
37
|
+
helperPaths.unshift(helperPath);
|
|
38
|
+
}
|
|
39
|
+
const parentDir = path.dirname(currentDir);
|
|
40
|
+
if (parentDir === currentDir) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
currentDir = parentDir;
|
|
44
|
+
}
|
|
45
|
+
return helperPaths;
|
|
46
|
+
}
|
|
47
|
+
async function findHelperInDir(dir) {
|
|
48
|
+
for (const ext of HELPER_EXTENSIONS) {
|
|
49
|
+
const filepath = path.join(dir, `${HELPER_FILENAME}${ext}`);
|
|
50
|
+
const [stats] = await attempt(() => stat(filepath));
|
|
51
|
+
if (stats?.isFile()) {
|
|
52
|
+
return filepath;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async function loadHelpers(fromDir, projectRoot) {
|
|
58
|
+
const helperPaths = await findHelperFiles(fromDir, projectRoot);
|
|
59
|
+
const merged = {};
|
|
60
|
+
for (const filepath of helperPaths) {
|
|
61
|
+
const [mod, err] = await attempt(() => loadJs(filepath));
|
|
62
|
+
if (err) {
|
|
63
|
+
observer.emit("error", {
|
|
64
|
+
source: "template",
|
|
65
|
+
error: err,
|
|
66
|
+
context: { filepath, operation: "load-helpers" }
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (mod && typeof mod === "object") {
|
|
71
|
+
const exports$1 = mod;
|
|
72
|
+
const exportCount = Object.keys(exports$1).length;
|
|
73
|
+
Object.assign(merged, exports$1);
|
|
74
|
+
observer.emit("template:helpers", {
|
|
75
|
+
filepath,
|
|
76
|
+
count: exportCount
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return merged;
|
|
81
|
+
}
|
|
82
|
+
async function loadJson5(filepath) {
|
|
83
|
+
const content = await readFile(filepath, "utf-8");
|
|
84
|
+
return JSON5.parse(content);
|
|
85
|
+
}
|
|
86
|
+
async function loadYaml(filepath) {
|
|
87
|
+
const content = await readFile(filepath, "utf-8");
|
|
88
|
+
return parse$1(content);
|
|
89
|
+
}
|
|
90
|
+
async function loadCsv(filepath) {
|
|
91
|
+
const content = await readFile(filepath, "utf-8");
|
|
92
|
+
return parse(content, {
|
|
93
|
+
columns: true,
|
|
94
|
+
skip_empty_lines: true,
|
|
95
|
+
trim: true
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function loadSql(filepath) {
|
|
99
|
+
return readFile(filepath, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/core/template/loaders/index.ts
|
|
103
|
+
var loaders = {
|
|
104
|
+
".json": loadJson5,
|
|
105
|
+
".json5": loadJson5,
|
|
106
|
+
".yaml": loadYaml,
|
|
107
|
+
".yml": loadYaml,
|
|
108
|
+
".csv": loadCsv,
|
|
109
|
+
".js": loadJs,
|
|
110
|
+
".mjs": loadJs,
|
|
111
|
+
".ts": loadJs,
|
|
112
|
+
".sql": loadSql
|
|
113
|
+
};
|
|
114
|
+
function hasLoader(ext) {
|
|
115
|
+
return ext in loaders;
|
|
116
|
+
}
|
|
117
|
+
function getLoader(ext) {
|
|
118
|
+
return loaders[ext];
|
|
119
|
+
}
|
|
120
|
+
async function loadDataFile(filepath) {
|
|
121
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
122
|
+
const loader = getLoader(ext);
|
|
123
|
+
if (!loader) {
|
|
124
|
+
throw new Error(`No loader registered for extension: ${ext}`);
|
|
125
|
+
}
|
|
126
|
+
return loader(filepath);
|
|
127
|
+
}
|
|
128
|
+
function toContextKey(filename) {
|
|
129
|
+
const ext = path.extname(filename);
|
|
130
|
+
const base = path.basename(filename, ext);
|
|
131
|
+
return v.camelCase(base);
|
|
132
|
+
}
|
|
133
|
+
function sqlEscape(value) {
|
|
134
|
+
return value.replace(/'/g, "''");
|
|
135
|
+
}
|
|
136
|
+
function sqlQuote(value) {
|
|
137
|
+
if (value === null) {
|
|
138
|
+
return "NULL";
|
|
139
|
+
}
|
|
140
|
+
return `'${sqlEscape(String(value))}'`;
|
|
141
|
+
}
|
|
142
|
+
function generateUuid() {
|
|
143
|
+
return crypto.randomUUID();
|
|
144
|
+
}
|
|
145
|
+
function isoNow() {
|
|
146
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/core/template/context.ts
|
|
150
|
+
async function buildContext(templatePath, options = {}) {
|
|
151
|
+
const templateDir = path.dirname(templatePath);
|
|
152
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
153
|
+
const helpers = await loadHelpers(templateDir, projectRoot);
|
|
154
|
+
const dataFiles = await loadDataFilesInDir(templateDir);
|
|
155
|
+
const hasLocalConfig = "config" in dataFiles;
|
|
156
|
+
const ctx = {
|
|
157
|
+
// Inherited helpers (can be overridden by data files with same name)
|
|
158
|
+
...helpers,
|
|
159
|
+
// Auto-loaded data files
|
|
160
|
+
...dataFiles,
|
|
161
|
+
// Config (only if no local config.* file)
|
|
162
|
+
...hasLocalConfig ? {} : { config: options.config },
|
|
163
|
+
// Secrets
|
|
164
|
+
secrets: options.secrets ?? {},
|
|
165
|
+
globalSecrets: options.globalSecrets ?? {},
|
|
166
|
+
// Environment
|
|
167
|
+
env: process.env,
|
|
168
|
+
// Built-in helpers
|
|
169
|
+
include: createIncludeHelper(templateDir, projectRoot, options),
|
|
170
|
+
escape: sqlEscape,
|
|
171
|
+
quote: sqlQuote,
|
|
172
|
+
json: (value) => JSON.stringify(value),
|
|
173
|
+
now: isoNow,
|
|
174
|
+
uuid: generateUuid
|
|
175
|
+
};
|
|
176
|
+
return ctx;
|
|
177
|
+
}
|
|
178
|
+
async function loadDataFilesInDir(dir) {
|
|
179
|
+
const data = {};
|
|
180
|
+
const [entries, readErr] = await attempt(() => readdir(dir, { withFileTypes: true }));
|
|
181
|
+
if (readErr || !entries) {
|
|
182
|
+
observer.emit("error", {
|
|
183
|
+
source: "template",
|
|
184
|
+
error: readErr ?? new Error("Failed to read directory"),
|
|
185
|
+
context: { dir, operation: "scan-data-files" }
|
|
186
|
+
});
|
|
187
|
+
return data;
|
|
188
|
+
}
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (!entry.isFile()) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (entry.name.startsWith(HELPER_FILENAME)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (entry.name.endsWith(".tmpl")) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
200
|
+
if (!hasLoader(ext)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ext === ".sql") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const filepath = path.join(dir, entry.name);
|
|
207
|
+
const key = toContextKey(entry.name);
|
|
208
|
+
const [loaded, loadErr] = await attempt(() => loadDataFile(filepath));
|
|
209
|
+
if (loadErr) {
|
|
210
|
+
observer.emit("error", {
|
|
211
|
+
source: "template",
|
|
212
|
+
error: loadErr,
|
|
213
|
+
context: { filepath, operation: "load-data-file" }
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
data[key] = loaded;
|
|
218
|
+
observer.emit("template:load", {
|
|
219
|
+
filepath,
|
|
220
|
+
format: ext
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return data;
|
|
224
|
+
}
|
|
225
|
+
function createIncludeHelper(templateDir, projectRoot, options) {
|
|
226
|
+
return async (includePath) => {
|
|
227
|
+
const resolved = path.resolve(templateDir, includePath);
|
|
228
|
+
if (!resolved.startsWith(projectRoot)) {
|
|
229
|
+
throw new Error(`Include path escapes project root: ${includePath}`);
|
|
230
|
+
}
|
|
231
|
+
if (resolved.endsWith(".tmpl")) {
|
|
232
|
+
const { processFile: processFile2 } = await import('./engine-B4JH5RQJ.js');
|
|
233
|
+
const result = await processFile2(resolved, options);
|
|
234
|
+
return result.sql;
|
|
235
|
+
}
|
|
236
|
+
const [content, err] = await attempt(() => loadDataFile(resolved));
|
|
237
|
+
if (err) {
|
|
238
|
+
throw new Error(`Failed to include '${includePath}': ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
if (typeof content === "string") {
|
|
241
|
+
return content;
|
|
242
|
+
}
|
|
243
|
+
return JSON.stringify(content);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/core/template/engine.ts
|
|
248
|
+
var eta = new Eta({
|
|
249
|
+
// Custom tags for code blocks
|
|
250
|
+
tags: ["{%", "%}"],
|
|
251
|
+
// Variable name for context ($ instead of it)
|
|
252
|
+
varName: "$",
|
|
253
|
+
// Don't auto-escape (SQL doesn't need HTML escaping)
|
|
254
|
+
autoEscape: false,
|
|
255
|
+
// Allow async functions in templates
|
|
256
|
+
useWith: false,
|
|
257
|
+
// Don't cache templates (we handle caching at a higher level)
|
|
258
|
+
cache: false
|
|
259
|
+
});
|
|
260
|
+
function isTemplate(filepath) {
|
|
261
|
+
return filepath.endsWith(TEMPLATE_EXTENSION);
|
|
262
|
+
}
|
|
263
|
+
async function renderTemplate(template, context) {
|
|
264
|
+
return eta.renderStringAsync(template, context);
|
|
265
|
+
}
|
|
266
|
+
async function processFile(filepath, options = {}) {
|
|
267
|
+
const content = await readFile(filepath, "utf-8");
|
|
268
|
+
if (!isTemplate(filepath)) {
|
|
269
|
+
return {
|
|
270
|
+
sql: content,
|
|
271
|
+
isTemplate: false
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const start = performance.now();
|
|
275
|
+
const context = await buildContext(filepath, options);
|
|
276
|
+
const sql = await renderTemplate(content, context);
|
|
277
|
+
const durationMs = performance.now() - start;
|
|
278
|
+
observer.emit("template:render", {
|
|
279
|
+
filepath,
|
|
280
|
+
durationMs
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
sql,
|
|
284
|
+
isTemplate: true,
|
|
285
|
+
durationMs
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function processFiles(filepaths, options = {}) {
|
|
289
|
+
const results = [];
|
|
290
|
+
for (const filepath of filepaths) {
|
|
291
|
+
const result = await processFile(filepath, options);
|
|
292
|
+
results.push(result);
|
|
293
|
+
}
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { eta, isTemplate, observer, processFile, processFiles, renderTemplate };
|
|
298
|
+
//# sourceMappingURL=chunk-MERTHCAJ.js.map
|
|
299
|
+
//# sourceMappingURL=chunk-MERTHCAJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/observer.ts","../../../src/core/template/types.ts","../../../src/core/template/loaders/js.ts","../../../src/core/template/helpers.ts","../../../src/core/template/loaders/json5.ts","../../../src/core/template/loaders/yaml.ts","../../../src/core/template/loaders/csv.ts","../../../src/core/template/loaders/sql.ts","../../../src/core/template/loaders/index.ts","../../../src/core/template/utils.ts","../../../src/core/template/context.ts","../../../src/core/template/engine.ts"],"names":["exports","readFile","parse","path","attempt","processFile"],"mappings":"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","file":"chunk-MERTHCAJ.js","sourcesContent":["/**\n * Central event system for noorm.\n *\n * Core modules emit events, CLI subscribes. This creates a clean separation\n * between business logic and UI concerns.\n *\n * @example\n * ```typescript\n * // In core module - emit events at key points\n * observer.emit('file:before', { filepath, checksum, configName })\n *\n * // In CLI - subscribe to events\n * const cleanup = observer.on('file:after', (data) => updateProgress(data))\n *\n * // Pattern matching for multiple events\n * observer.on(/^file:/, ({ event, data }) => logFileEvent(event, data))\n * ```\n */\nimport { ObserverEngine, type Events } from '@logosdx/observer';\n\nimport type { SettingsEvents } from './settings/index.js';\nimport type { AppMode, ShutdownReason, ShutdownPhase, PhaseStatus } from './lifecycle/types.js';\n\n/**\n * All events emitted by noorm core modules.\n *\n * Events are namespaced by module:\n * - `file:*` - Individual SQL file execution\n * - `build:*` - Schema build operations\n * - `run:*` - Ad-hoc file/dir execution\n * - `change:*` - Change execution\n * - `lock:*` - Lock acquisition/release\n * - `state:*` - State load/persist\n * - `config:*` - Config CRUD\n * - `secret:*` - Secret CRUD\n * - `db:*` - Database lifecycle\n * - `template:*` - Template rendering\n * - `identity:*` - Identity resolution\n * - `connection:*` - Database connections\n * - `settings:*` - Settings lifecycle and mutations\n * - `error` - Catch-all errors\n */\nexport interface NoormEvents extends SettingsEvents {\n // File execution\n 'file:before': { filepath: string; checksum: string; configName: string };\n 'file:after': {\n filepath: string;\n status: 'success' | 'failed';\n durationMs: number;\n error?: string;\n };\n 'file:skip': { filepath: string; reason: 'unchanged' | 'already-run' };\n 'file:dry-run': { filepath: string; outputPath: string };\n\n // Change lifecycle\n 'change:created': { name: string; path: string };\n 'change:start': { name: string; direction: 'change' | 'revert'; files: string[] };\n 'change:file': { change: string; filepath: string; index: number; total: number };\n 'change:complete': {\n name: string;\n direction: 'change' | 'revert';\n status: 'success' | 'failed';\n durationMs: number;\n };\n 'change:skip': { name: string; reason: string };\n\n // Build/Run\n 'build:start': { sqlPath: string; fileCount: number };\n 'build:complete': {\n status: 'success' | 'failed' | 'partial';\n filesRun: number;\n filesSkipped: number;\n filesFailed: number;\n durationMs: number;\n };\n 'run:file': { filepath: string; configName: string };\n 'run:dir': { dirpath: string; fileCount: number; configName: string };\n\n // Lock\n 'lock:acquiring': { configName: string; identity: string };\n 'lock:acquired': { configName: string; identity: string; expiresAt: Date };\n 'lock:released': { configName: string; identity: string };\n 'lock:blocked': { configName: string; holder: string; heldSince: Date };\n 'lock:expired': { configName: string; previousHolder: string };\n\n // State\n 'state:loaded': { configCount: number; activeConfig: string | null; version: string };\n 'state:persisted': { configCount: number };\n 'state:migrated': { from: string; to: string };\n\n // Config\n 'config:created': { name: string };\n 'config:updated': { name: string; fields: string[] };\n 'config:deleted': { name: string };\n 'config:activated': { name: string; previous: string | null };\n\n // Secrets (config-scoped)\n 'secret:set': { configName: string; key: string };\n 'secret:deleted': { configName: string; key: string };\n\n // Global secrets (app-level)\n 'global-secret:set': { key: string };\n 'global-secret:deleted': { key: string };\n\n // Known users\n 'known-user:added': { email: string; source: string };\n\n // DB lifecycle\n 'db:creating': { configName: string; database: string };\n 'db:created': { configName: string; database: string; durationMs: number };\n 'db:destroying': { configName: string; database: string };\n 'db:destroyed': { configName: string; database: string };\n 'db:bootstrap': { configName: string; tables: string[] };\n\n // Template\n 'template:render': { filepath: string; durationMs: number };\n 'template:load': { filepath: string; format: string };\n 'template:helpers': { filepath: string; count: number };\n\n // Identity (audit)\n 'identity:resolved': {\n name: string;\n email?: string;\n source: 'state' | 'git' | 'system' | 'config' | 'env';\n };\n\n // Identity (cryptographic)\n 'identity:created': { identityHash: string; name: string; email: string; machine: string };\n 'identity:registered': { identityHash: string; name: string; email: string };\n 'identity:synced': { configName: string; registered: boolean; knownUsersCount: number };\n\n // Connection\n 'connection:open': { configName: string; dialect: string };\n 'connection:close': { configName: string };\n 'connection:error': { configName: string; error: string };\n\n // App lifecycle\n 'app:starting': { mode: AppMode };\n 'app:ready': { mode: AppMode; startedAt: Date };\n 'app:shutdown': { reason: ShutdownReason; exitCode: number };\n 'app:shutdown:phase': {\n phase: ShutdownPhase;\n status: PhaseStatus;\n durationMs?: number;\n error?: Error;\n };\n 'app:exit': { code: number };\n 'app:fatal': { error: Error; type?: 'exception' | 'rejection' };\n\n // Router\n 'router:navigated': { from: string; to: string; params: Record<string, string | number | boolean | undefined> };\n 'router:popped': { popped: string; to: string };\n\n // Errors\n error: { source: string; error: Error; context?: Record<string, unknown> };\n}\n\nexport type NoormEventNames = Events<NoormEvents>;\nexport type NoormEventCallback<E extends NoormEventNames> = ObserverEngine.EventCallback<\n NoormEvents[E]\n>;\n\n/**\n * Global observer instance for noorm.\n *\n * Enable debug mode with `NOORM_DEBUG=1` to see all events as they occur.\n *\n * @example\n * ```typescript\n * import { observer } from './observer'\n *\n * // Emit an event\n * observer.emit('file:before', { filepath, checksum, configName })\n *\n * // Subscribe to an event\n * const cleanup = observer.on('file:after', (data) => {\n * console.log(`File ${data.filepath}: ${data.status}`)\n * })\n *\n * // Clean up when done\n * cleanup()\n * ```\n */\nexport const observer = new ObserverEngine<NoormEvents>({\n name: 'noorm',\n spy: process.env['NOORM_DEBUG']\n ? (action) => console.error(`[noorm:${action.fn}] ${String(action.event)}`)\n : undefined,\n});\n\n// ? Not against this, but why are we re-exporting? convenience\nexport type { ObserverEngine };\n","/**\n * Template engine types.\n *\n * Defines the context object ($) available in templates, loader interfaces,\n * and configuration options for the template engine.\n */\n\n/**\n * Built-in helper functions available on the template context.\n */\nexport interface BuiltInHelpers {\n /**\n * Include another SQL file.\n * Path is resolved relative to the template's directory.\n *\n * @example\n * ```sql\n * {%~ await $.include('lib/uuid_function.sql') %}\n * ```\n */\n include: (path: string) => Promise<string>;\n\n /**\n * SQL-escape a string value.\n * Escapes single quotes by doubling them.\n *\n * @example\n * ```sql\n * WHERE name = '{%~ $.escape(userName) %}'\n * ```\n */\n escape: (value: string) => string;\n\n /**\n * SQL-escape and wrap in single quotes.\n *\n * @example\n * ```sql\n * INSERT INTO users (name) VALUES ({%~ $.quote(userName) %});\n * ```\n */\n quote: (value: string | number | boolean | null) => string;\n\n /**\n * JSON stringify a value.\n *\n * @example\n * ```sql\n * INSERT INTO config (data) VALUES ('{%~ $.json(configObject) %}');\n * ```\n */\n json: (value: unknown) => string;\n\n /**\n * Current ISO timestamp.\n *\n * @example\n * ```sql\n * INSERT INTO logs (created_at) VALUES ('{%~ $.now() %}');\n * ```\n */\n now: () => string;\n\n /**\n * Generate a UUID v4.\n *\n * @example\n * ```sql\n * INSERT INTO users (id) VALUES ('{%~ $.uuid() %}');\n * ```\n */\n uuid: () => string;\n}\n\n/**\n * Template context object ($) available in templates.\n *\n * Contains auto-loaded data, inherited helpers, secrets, and built-in functions.\n */\nexport interface TemplateContext extends BuiltInHelpers {\n /**\n * Active configuration object.\n * Only available if no `config.*` file exists in the template directory.\n */\n config?: Record<string, unknown>;\n\n /**\n * Decrypted secrets for the active config.\n */\n secrets: Record<string, string>;\n\n /**\n * Decrypted global secrets (shared across configs).\n */\n globalSecrets: Record<string, string>;\n\n /**\n * Environment variables.\n */\n env: Record<string, string | undefined>;\n\n /**\n * Auto-loaded data files and inherited helpers.\n * Keys are camelCased filenames (e.g., `$.users`, `$.seedData`).\n */\n [key: string]: unknown;\n}\n\n/**\n * Result from a data loader.\n */\nexport interface LoaderResult {\n /**\n * The parsed data.\n */\n data: unknown;\n\n /**\n * Original file path.\n */\n filepath: string;\n\n /**\n * File extension (e.g., '.json5', '.yml').\n */\n format: string;\n}\n\n/**\n * A data file loader function.\n */\nexport type Loader = (filepath: string) => Promise<unknown>;\n\n/**\n * Registry of loaders by file extension.\n */\nexport type LoaderRegistry = Record<string, Loader>;\n\n/**\n * Options for rendering a template.\n */\nexport interface RenderOptions {\n /**\n * Active configuration to include in context.\n */\n config?: Record<string, unknown>;\n\n /**\n * Secrets for the active config.\n */\n secrets?: Record<string, string>;\n\n /**\n * Global secrets shared across configs.\n */\n globalSecrets?: Record<string, string>;\n\n /**\n * Project root directory.\n * Used to determine where to stop walking up for helpers.\n * Defaults to process.cwd().\n */\n projectRoot?: string;\n}\n\n/**\n * Result of processing a SQL file.\n */\nexport interface ProcessResult {\n /**\n * The SQL content (rendered if template, raw if .sql).\n */\n sql: string;\n\n /**\n * Whether the file was a template.\n */\n isTemplate: boolean;\n\n /**\n * Render duration in milliseconds (only for templates).\n */\n durationMs?: number;\n}\n\n/**\n * Supported data file extensions.\n */\nexport const DATA_EXTENSIONS = [\n '.json',\n '.json5',\n '.yaml',\n '.yml',\n '.csv',\n '.js',\n '.mjs',\n '.ts',\n '.sql',\n] as const;\n\n/**\n * Template file extension.\n */\nexport const TEMPLATE_EXTENSION = '.tmpl';\n\n/**\n * Helper file name pattern.\n */\nexport const HELPER_FILENAME = '$helpers';\n\n/**\n * Supported helper file extensions.\n */\nexport const HELPER_EXTENSIONS = ['.ts', '.js', '.mjs'] as const;\n","/**\n * JavaScript/TypeScript module loader.\n *\n * Loads .js, .mjs, and .ts files via dynamic import.\n * Returns module.default if available, otherwise the entire module.\n *\n * @example\n * ```typescript\n * const data = await loadJs('/path/to/helpers.ts')\n * ```\n */\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Load a JavaScript or TypeScript module.\n *\n * Uses dynamic import to load the module. If the module has a default\n * export, returns that. Otherwise returns the entire module object.\n *\n * @param filepath - Absolute path to the JS/TS file\n * @returns The module's default export or the entire module\n * @throws If file cannot be imported\n */\nexport async function loadJs(filepath: string): Promise<unknown> {\n\n // Convert to file URL for cross-platform compatibility\n const url = pathToFileURL(filepath).href;\n\n // Add cache-busting query param to avoid stale imports\n const urlWithCacheBust = `${url}?t=${Date.now()}`;\n\n const mod = await import(urlWithCacheBust);\n\n // Return default export if available, otherwise the whole module\n return mod.default !== undefined ? mod.default : mod;\n\n}\n","/**\n * Helper file tree walker.\n *\n * Walks up the directory tree from a template's location to the project root,\n * collecting and merging $helpers.ts files. Child helpers override parent helpers.\n *\n * @example\n * ```typescript\n * import { loadHelpers } from './helpers'\n *\n * // Given structure:\n * // sql/\n * // ├── $helpers.ts ← loaded first (base)\n * // └── users/\n * // ├── $helpers.ts ← loaded second (overrides)\n * // └── 001_create.sql.tmpl\n *\n * const helpers = await loadHelpers('/project/sql/users', '/project')\n * // helpers contains merged exports from both files\n * ```\n */\nimport path from 'node:path';\nimport { stat } from 'node:fs/promises';\n\nimport { attempt } from '@logosdx/utils';\n\nimport { observer } from '../observer.js';\nimport { HELPER_FILENAME, HELPER_EXTENSIONS } from './types.js';\nimport { loadJs } from './loaders/js.js';\n\n/**\n * Find all helper files from a directory up to the project root.\n *\n * Returns paths in order from root to leaf (so child can override parent).\n *\n * @param fromDir - Starting directory (template's directory)\n * @param projectRoot - Project root directory (stop walking here)\n * @returns Array of helper file paths, ordered root to leaf\n */\nexport async function findHelperFiles(fromDir: string, projectRoot: string): Promise<string[]> {\n\n const helperPaths: string[] = [];\n let currentDir = path.resolve(fromDir);\n const root = path.resolve(projectRoot);\n\n // Walk up until we reach or pass the project root\n while (currentDir.startsWith(root)) {\n\n const helperPath = await findHelperInDir(currentDir);\n\n if (helperPath) {\n\n // Prepend so we get root-to-leaf order\n helperPaths.unshift(helperPath);\n\n }\n\n // Move up one directory\n const parentDir = path.dirname(currentDir);\n\n // Stop if we've reached the root or can't go higher\n if (parentDir === currentDir) {\n\n break;\n\n }\n\n currentDir = parentDir;\n\n }\n\n return helperPaths;\n\n}\n\n/**\n * Find a helper file in a directory.\n *\n * Checks for $helpers.ts, $helpers.js, $helpers.mjs in order.\n *\n * @param dir - Directory to search\n * @returns Path to helper file if found, null otherwise\n */\nasync function findHelperInDir(dir: string): Promise<string | null> {\n\n for (const ext of HELPER_EXTENSIONS) {\n\n const filepath = path.join(dir, `${HELPER_FILENAME}${ext}`);\n const [stats] = await attempt(() => stat(filepath));\n\n if (stats?.isFile()) {\n\n return filepath;\n\n }\n\n }\n\n return null;\n\n}\n\n/**\n * Load and merge helper files from a directory tree.\n *\n * Walks up from the template directory to the project root, loading each\n * $helpers.ts file found. Later helpers (closer to template) override\n * earlier helpers (closer to root).\n *\n * @param fromDir - Template's directory\n * @param projectRoot - Project root directory\n * @returns Merged helper exports\n *\n * @example\n * ```typescript\n * const helpers = await loadHelpers('/project/sql/users', '/project')\n * // helpers.padId() from sql/users/$helpers.ts\n * // helpers.formatDate() from sql/$helpers.ts\n * ```\n */\nexport async function loadHelpers(\n fromDir: string,\n projectRoot: string,\n): Promise<Record<string, unknown>> {\n\n const helperPaths = await findHelperFiles(fromDir, projectRoot);\n const merged: Record<string, unknown> = {};\n\n for (const filepath of helperPaths) {\n\n const [mod, err] = await attempt(() => loadJs(filepath));\n\n if (err) {\n\n observer.emit('error', {\n source: 'template',\n error: err,\n context: { filepath, operation: 'load-helpers' },\n });\n continue;\n\n }\n\n // Merge exports (later files override earlier)\n if (mod && typeof mod === 'object') {\n\n const exports = mod as Record<string, unknown>;\n const exportCount = Object.keys(exports).length;\n\n Object.assign(merged, exports);\n\n observer.emit('template:helpers', {\n filepath,\n count: exportCount,\n });\n\n }\n\n }\n\n return merged;\n\n}\n","/**\n * JSON5 data loader.\n *\n * Loads .json and .json5 files using the JSON5 parser, which supports:\n * - Comments (single-line and block)\n * - Trailing commas\n * - Unquoted keys\n * - Single-quoted strings\n * - Multi-line strings\n *\n * @example\n * ```typescript\n * const data = await loadJson5('/path/to/config.json5')\n * ```\n */\nimport { readFile } from 'node:fs/promises';\n\nimport JSON5 from 'json5';\n\n/**\n * Load and parse a JSON5 file.\n *\n * @param filepath - Absolute path to the JSON5 file\n * @returns Parsed JSON5 data\n * @throws If file cannot be read or parsed\n */\nexport async function loadJson5(filepath: string): Promise<unknown> {\n\n const content = await readFile(filepath, 'utf-8');\n\n return JSON5.parse(content);\n\n}\n","/**\n * YAML data loader.\n *\n * Loads .yaml and .yml files using the yaml parser.\n *\n * @example\n * ```typescript\n * const data = await loadYaml('/path/to/config.yml')\n * ```\n */\nimport { readFile } from 'node:fs/promises';\n\nimport { parse } from 'yaml';\n\n/**\n * Load and parse a YAML file.\n *\n * @param filepath - Absolute path to the YAML file\n * @returns Parsed YAML data\n * @throws If file cannot be read or parsed\n */\nexport async function loadYaml(filepath: string): Promise<unknown> {\n\n const content = await readFile(filepath, 'utf-8');\n\n return parse(content);\n\n}\n","/**\n * CSV data loader.\n *\n * Loads .csv files using csv-parse with headers.\n * Returns an array of objects where keys are column headers.\n *\n * @example\n * ```typescript\n * const data = await loadCsv('/path/to/users.csv')\n * // → [{ name: 'Alice', email: 'alice@example.com' }, ...]\n * ```\n */\nimport { readFile } from 'node:fs/promises';\n\nimport { parse } from 'csv-parse/sync';\n\n/**\n * Load and parse a CSV file.\n *\n * @param filepath - Absolute path to the CSV file\n * @returns Array of row objects with header keys\n * @throws If file cannot be read or parsed\n */\nexport async function loadCsv(filepath: string): Promise<Record<string, string>[]> {\n\n const content = await readFile(filepath, 'utf-8');\n\n return parse(content, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n });\n\n}\n","/**\n * SQL file loader.\n *\n * Loads .sql files as raw text strings.\n * Used for including SQL fragments in templates.\n *\n * @example\n * ```typescript\n * const sql = await loadSql('/path/to/fragment.sql')\n * ```\n */\nimport { readFile } from 'node:fs/promises';\n\n/**\n * Load a SQL file as text.\n *\n * @param filepath - Absolute path to the SQL file\n * @returns The SQL file content as a string\n * @throws If file cannot be read\n */\nexport async function loadSql(filepath: string): Promise<string> {\n\n return readFile(filepath, 'utf-8');\n\n}\n","/**\n * Loader registry for data files.\n *\n * Maps file extensions to their respective loader functions.\n * Provides a unified interface for loading any supported data format.\n *\n * @example\n * ```typescript\n * import { loadDataFile, getLoader, hasLoader } from './loaders'\n *\n * if (hasLoader('.json5')) {\n * const data = await loadDataFile('/path/to/config.json5')\n * }\n * ```\n */\nimport path from 'node:path';\n\nimport type { Loader, LoaderRegistry } from '../types.js';\nimport { loadJson5 } from './json5.js';\nimport { loadYaml } from './yaml.js';\nimport { loadCsv } from './csv.js';\nimport { loadJs } from './js.js';\nimport { loadSql } from './sql.js';\n\n/**\n * Registry of loaders by file extension.\n */\nconst loaders: LoaderRegistry = {\n '.json': loadJson5,\n '.json5': loadJson5,\n '.yaml': loadYaml,\n '.yml': loadYaml,\n '.csv': loadCsv,\n '.js': loadJs,\n '.mjs': loadJs,\n '.ts': loadJs,\n '.sql': loadSql,\n};\n\n/**\n * Check if a loader exists for the given extension.\n *\n * @param ext - File extension (e.g., '.json5')\n * @returns True if a loader is registered for this extension\n */\nexport function hasLoader(ext: string): boolean {\n\n return ext in loaders;\n\n}\n\n/**\n * Get the loader function for a file extension.\n *\n * @param ext - File extension (e.g., '.json5')\n * @returns The loader function, or undefined if not found\n */\nexport function getLoader(ext: string): Loader | undefined {\n\n return loaders[ext];\n\n}\n\n/**\n * Load a data file using the appropriate loader.\n *\n * Determines the loader from the file extension and loads the file.\n *\n * @param filepath - Absolute path to the data file\n * @returns The loaded and parsed data\n * @throws If no loader exists for the file extension\n * @throws If the file cannot be loaded or parsed\n *\n * @example\n * ```typescript\n * const users = await loadDataFile('/path/to/users.json5')\n * const config = await loadDataFile('/path/to/config.yml')\n * ```\n */\nexport async function loadDataFile(filepath: string): Promise<unknown> {\n\n const ext = path.extname(filepath).toLowerCase();\n const loader = getLoader(ext);\n\n if (!loader) {\n\n throw new Error(`No loader registered for extension: ${ext}`);\n\n }\n\n return loader(filepath);\n\n}\n\n/**\n * Get all supported data file extensions.\n *\n * @returns Array of supported extensions (e.g., ['.json', '.json5', '.yml', ...])\n */\nexport function getSupportedExtensions(): string[] {\n\n return Object.keys(loaders);\n\n}\n\n// Re-export individual loaders for direct use\nexport { loadJson5 } from './json5.js';\nexport { loadYaml } from './yaml.js';\nexport { loadCsv } from './csv.js';\nexport { loadJs } from './js.js';\nexport { loadSql } from './sql.js';\n","/**\n * String transformation utilities using Voca.\n *\n * Provides consistent string transformations for converting filenames\n * to context property names.\n *\n * @example\n * ```typescript\n * import { toContextKey } from './utils'\n *\n * toContextKey('my-config.json5') // → 'myConfig'\n * toContextKey('seed_data.yml') // → 'seedData'\n * toContextKey('API_KEYS.json') // → 'apiKeys'\n * ```\n */\nimport v from 'voca';\nimport path from 'node:path';\n\n/**\n * Convert a filename to a camelCase context key.\n *\n * Strips the file extension and converts the base name to camelCase.\n * Handles kebab-case, snake_case, and SCREAMING_CASE.\n *\n * @param filename - The filename to convert (e.g., 'my-config.json5')\n * @returns The camelCase key (e.g., 'myConfig')\n *\n * @example\n * ```typescript\n * toContextKey('my-config.json5') // → 'myConfig'\n * toContextKey('seed_data.yml') // → 'seedData'\n * toContextKey('API_KEYS.json') // → 'apiKeys'\n * toContextKey('users.csv') // → 'users'\n * ```\n */\nexport function toContextKey(filename: string): string {\n\n // Get basename without extension\n const ext = path.extname(filename);\n const base = path.basename(filename, ext);\n\n // Convert to camelCase\n return v.camelCase(base);\n\n}\n\n/**\n * SQL-escape a string value.\n *\n * Escapes single quotes by doubling them, which is the standard\n * SQL escape sequence for string literals.\n *\n * @param value - The string to escape\n * @returns The escaped string (without surrounding quotes)\n *\n * @example\n * ```typescript\n * sqlEscape(\"O'Brien\") // → \"O''Brien\"\n * sqlEscape(\"normal\") // → \"normal\"\n * ```\n */\nexport function sqlEscape(value: string): string {\n\n return value.replace(/'/g, \"''\");\n\n}\n\n/**\n * SQL-escape and wrap in single quotes.\n *\n * Handles null values and various types appropriately.\n *\n * @param value - The value to quote\n * @returns The quoted SQL literal\n *\n * @example\n * ```typescript\n * sqlQuote(\"O'Brien\") // → \"'O''Brien'\"\n * sqlQuote(42) // → \"'42'\"\n * sqlQuote(null) // → \"NULL\"\n * sqlQuote(true) // → \"'true'\"\n * ```\n */\nexport function sqlQuote(value: string | number | boolean | null): string {\n\n if (value === null) {\n\n return 'NULL';\n\n }\n\n return `'${sqlEscape(String(value))}'`;\n\n}\n\n/**\n * Generate a UUID v4.\n *\n * Uses crypto.randomUUID() for secure random generation.\n *\n * @returns A UUID v4 string\n *\n * @example\n * ```typescript\n * generateUuid() // → \"550e8400-e29b-41d4-a716-446655440000\"\n * ```\n */\nexport function generateUuid(): string {\n\n return crypto.randomUUID();\n\n}\n\n/**\n * Get current ISO timestamp.\n *\n * @returns ISO 8601 timestamp string\n *\n * @example\n * ```typescript\n * isoNow() // → \"2024-01-15T10:30:00.000Z\"\n * ```\n */\nexport function isoNow(): string {\n\n return new Date().toISOString();\n\n}\n","/**\n * Template context builder.\n *\n * Builds the $ context object available in templates by:\n * 1. Loading inherited helpers from $helpers.ts files\n * 2. Auto-loading data files from the template's directory\n * 3. Adding config, secrets, env, and built-in helpers\n *\n * @example\n * ```typescript\n * import { buildContext } from './context'\n *\n * const ctx = await buildContext('/project/sql/users/001_create.sql.tmpl', {\n * projectRoot: '/project',\n * config: activeConfig,\n * secrets: { API_KEY: '...' },\n * })\n *\n * // ctx now has: $.padId, $.users, $.config, $.secrets, $.quote, etc.\n * ```\n */\nimport path from 'node:path';\nimport { readdir } from 'node:fs/promises';\n\nimport { attempt } from '@logosdx/utils';\n\nimport { observer } from '../observer.js';\nimport type { TemplateContext, RenderOptions } from './types.js';\nimport { HELPER_FILENAME } from './types.js';\nimport { loadHelpers } from './helpers.js';\nimport { loadDataFile, hasLoader } from './loaders/index.js';\nimport { toContextKey, sqlEscape, sqlQuote, generateUuid, isoNow } from './utils.js';\n\n/**\n * Build the template context ($) for a template file.\n *\n * @param templatePath - Absolute path to the template file\n * @param options - Render options (config, secrets, projectRoot)\n * @returns The complete template context\n */\nexport async function buildContext(\n templatePath: string,\n options: RenderOptions = {},\n): Promise<TemplateContext> {\n\n const templateDir = path.dirname(templatePath);\n const projectRoot = options.projectRoot ?? process.cwd();\n\n // 1. Load inherited helpers\n const helpers = await loadHelpers(templateDir, projectRoot);\n\n // 2. Auto-load data files from template directory\n const dataFiles = await loadDataFilesInDir(templateDir);\n\n // 3. Check if data files include a config file\n const hasLocalConfig = 'config' in dataFiles;\n\n // 4. Build context with all components\n const ctx: TemplateContext = {\n // Inherited helpers (can be overridden by data files with same name)\n ...helpers,\n\n // Auto-loaded data files\n ...dataFiles,\n\n // Config (only if no local config.* file)\n ...(hasLocalConfig ? {} : { config: options.config }),\n\n // Secrets\n secrets: options.secrets ?? {},\n globalSecrets: options.globalSecrets ?? {},\n\n // Environment\n env: process.env as Record<string, string | undefined>,\n\n // Built-in helpers\n include: createIncludeHelper(templateDir, projectRoot, options),\n escape: sqlEscape,\n quote: sqlQuote,\n json: (value: unknown) => JSON.stringify(value),\n now: isoNow,\n uuid: generateUuid,\n };\n\n return ctx;\n\n}\n\n/**\n * Load all data files in a directory.\n *\n * Scans the directory for supported data file extensions and loads each one.\n * File names are converted to camelCase context keys.\n *\n * @param dir - Directory to scan\n * @returns Object with camelCased keys and loaded data\n */\nasync function loadDataFilesInDir(dir: string): Promise<Record<string, unknown>> {\n\n const data: Record<string, unknown> = {};\n\n const [entries, readErr] = await attempt(() => readdir(dir, { withFileTypes: true }));\n\n if (readErr || !entries) {\n\n observer.emit('error', {\n source: 'template',\n error: readErr ?? new Error('Failed to read directory'),\n context: { dir, operation: 'scan-data-files' },\n });\n\n return data;\n\n }\n\n for (const entry of entries) {\n\n // Skip directories\n if (!entry.isFile()) {\n\n continue;\n\n }\n\n // Skip helper files\n if (entry.name.startsWith(HELPER_FILENAME)) {\n\n continue;\n\n }\n\n // Skip template files\n if (entry.name.endsWith('.tmpl')) {\n\n continue;\n\n }\n\n const ext = path.extname(entry.name).toLowerCase();\n\n // Skip unsupported extensions\n if (!hasLoader(ext)) {\n\n continue;\n\n }\n\n // Skip .sql files in data loading (they're for include())\n if (ext === '.sql') {\n\n continue;\n\n }\n\n const filepath = path.join(dir, entry.name);\n const key = toContextKey(entry.name);\n\n const [loaded, loadErr] = await attempt(() => loadDataFile(filepath));\n\n if (loadErr) {\n\n observer.emit('error', {\n source: 'template',\n error: loadErr,\n context: { filepath, operation: 'load-data-file' },\n });\n continue;\n\n }\n\n data[key] = loaded;\n\n observer.emit('template:load', {\n filepath,\n format: ext,\n });\n\n }\n\n return data;\n\n}\n\n/**\n * Create the include() helper function.\n *\n * The include helper resolves paths relative to the template's directory\n * and cannot escape the project root. If the included file is a template\n * (.sql.tmpl), it will be rendered recursively with the same options.\n *\n * @param templateDir - Template's directory\n * @param projectRoot - Project root (cannot escape)\n * @param options - Render options for nested templates\n * @returns The include helper function\n */\nfunction createIncludeHelper(\n templateDir: string,\n projectRoot: string,\n options: RenderOptions,\n): (includePath: string) => Promise<string> {\n\n return async (includePath: string): Promise<string> => {\n\n // Resolve path relative to template directory\n const resolved = path.resolve(templateDir, includePath);\n\n // Security: ensure we don't escape project root\n if (!resolved.startsWith(projectRoot)) {\n\n throw new Error(`Include path escapes project root: ${includePath}`);\n\n }\n\n // If it's a template, render it recursively\n if (resolved.endsWith('.tmpl')) {\n\n // Dynamic import to avoid circular dependency\n const { processFile } = await import('./engine.js');\n const result = await processFile(resolved, options);\n\n return result.sql;\n\n }\n\n // Load raw file\n const [content, err] = await attempt(() => loadDataFile(resolved));\n\n if (err) {\n\n throw new Error(`Failed to include '${includePath}': ${err.message}`);\n\n }\n\n if (typeof content === 'string') {\n\n return content;\n\n }\n\n // Non-string content (shouldn't happen for .sql files)\n return JSON.stringify(content);\n\n };\n\n}\n","/**\n * Template engine using Eta.\n *\n * Wraps Eta with noorm's custom syntax and integrates with the context builder\n * for auto-loading data files and inherited helpers.\n *\n * @example\n * ```typescript\n * import { processFile, renderTemplate } from './engine'\n *\n * // Process any SQL file (template or raw)\n * const result = await processFile('/path/to/file.sql.tmpl', {\n * config: activeConfig,\n * secrets: { API_KEY: '...' },\n * })\n *\n * // Or render a template string directly\n * const sql = await renderTemplate(\n * '{% for (const r of $.roles) { %}...',\n * context,\n * )\n * ```\n */\nimport { readFile } from 'node:fs/promises';\n\nimport { Eta } from 'eta';\n\nimport { observer } from '../observer.js';\nimport type { TemplateContext, RenderOptions, ProcessResult } from './types.js';\nimport { TEMPLATE_EXTENSION } from './types.js';\nimport { buildContext } from './context.js';\n\n/**\n * Eta instance configured with noorm's custom syntax.\n *\n * Custom delimiters:\n * - `{% %}` for JavaScript code (instead of `<% %>`)\n * - `{%~ %}` for raw output (instead of `<%~ %>`)\n * - `$` as the context variable (instead of `it`)\n */\nconst eta = new Eta({\n // Custom tags for code blocks\n tags: ['{%', '%}'],\n\n // Variable name for context ($ instead of it)\n varName: '$',\n\n // Don't auto-escape (SQL doesn't need HTML escaping)\n autoEscape: false,\n\n // Allow async functions in templates\n useWith: false,\n\n // Don't cache templates (we handle caching at a higher level)\n cache: false,\n});\n\n/**\n * Check if a file path is a template.\n *\n * @param filepath - File path to check\n * @returns True if the file has the .tmpl extension\n */\nexport function isTemplate(filepath: string): boolean {\n\n return filepath.endsWith(TEMPLATE_EXTENSION);\n\n}\n\n/**\n * Render a template string with the given context.\n *\n * @param template - Template string with Eta syntax\n * @param context - The $ context object\n * @returns Rendered SQL string\n *\n * @example\n * ```typescript\n * const sql = await renderTemplate(\n * '{% for (const role of $.roles) { %}INSERT INTO roles (name) VALUES ({%~ $.quote(role) %});\\n{% } %}',\n * { roles: ['admin', 'user'], quote: sqlQuote }\n * )\n * ```\n */\nexport async function renderTemplate(template: string, context: TemplateContext): Promise<string> {\n\n return eta.renderStringAsync(template, context);\n\n}\n\n/**\n * Process a SQL file.\n *\n * If the file is a template (.sql.tmpl), renders it with the context.\n * If it's a raw SQL file (.sql), returns the content as-is.\n *\n * @param filepath - Absolute path to the SQL file\n * @param options - Render options (config, secrets, projectRoot)\n * @returns Process result with SQL content and metadata\n *\n * @example\n * ```typescript\n * const result = await processFile('/project/sql/users/001_create.sql.tmpl', {\n * projectRoot: '/project',\n * config: { name: 'dev', connection: { ... } },\n * secrets: { API_KEY: 'secret123' },\n * })\n *\n * console.log(result.sql) // Rendered SQL\n * console.log(result.isTemplate) // true\n * console.log(result.durationMs) // 12\n * ```\n */\nexport async function processFile(\n filepath: string,\n options: RenderOptions = {},\n): Promise<ProcessResult> {\n\n // Read file content\n const content = await readFile(filepath, 'utf-8');\n\n // If not a template, return raw content\n if (!isTemplate(filepath)) {\n\n return {\n sql: content,\n isTemplate: false,\n };\n\n }\n\n // It's a template - build context and render\n const start = performance.now();\n\n const context = await buildContext(filepath, options);\n const sql = await renderTemplate(content, context);\n\n const durationMs = performance.now() - start;\n\n observer.emit('template:render', {\n filepath,\n durationMs,\n });\n\n return {\n sql,\n isTemplate: true,\n durationMs,\n };\n\n}\n\n/**\n * Process multiple SQL files.\n *\n * @param filepaths - Array of file paths to process\n * @param options - Render options\n * @returns Array of process results\n */\nexport async function processFiles(\n filepaths: string[],\n options: RenderOptions = {},\n): Promise<ProcessResult[]> {\n\n const results: ProcessResult[] = [];\n\n for (const filepath of filepaths) {\n\n const result = await processFile(filepath, options);\n results.push(result);\n\n }\n\n return results;\n\n}\n\n// Export the Eta instance for advanced usage\nexport { eta };\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"engine-B4JH5RQJ.js"}
|