@buenojs/bueno 0.8.4 → 0.8.6
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/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Template Renderer
|
|
3
|
+
*
|
|
4
|
+
* Lightweight renderer for:
|
|
5
|
+
* - Variable interpolation: {{ variable }}
|
|
6
|
+
* - Filters: {{ value | uppercase | trim }}
|
|
7
|
+
* - Conditionals: {{ if condition }} ... {{ endif }}
|
|
8
|
+
*
|
|
9
|
+
* No dependencies, ~180 lines
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FilterRegistry, TemplateData } from "../types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Built-in filters
|
|
16
|
+
*/
|
|
17
|
+
const builtinFilters: FilterRegistry = {
|
|
18
|
+
uppercase: (value) => String(value).toUpperCase(),
|
|
19
|
+
lowercase: (value) => String(value).toLowerCase(),
|
|
20
|
+
capitalize: (value) => {
|
|
21
|
+
const str = String(value);
|
|
22
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
23
|
+
},
|
|
24
|
+
trim: (value) => String(value).trim(),
|
|
25
|
+
reverse: (value) => String(value).split("").reverse().join(""),
|
|
26
|
+
length: (value) => {
|
|
27
|
+
if (Array.isArray(value)) return value.length;
|
|
28
|
+
if (typeof value === "string") return value.length;
|
|
29
|
+
return 0;
|
|
30
|
+
},
|
|
31
|
+
join: (value, separator = ",") => {
|
|
32
|
+
if (Array.isArray(value)) return value.join(String(separator));
|
|
33
|
+
return String(value);
|
|
34
|
+
},
|
|
35
|
+
slice: (value, start = 0, end = undefined) => {
|
|
36
|
+
const str = String(value);
|
|
37
|
+
return str.slice(Number(start), end ? Number(end) : undefined);
|
|
38
|
+
},
|
|
39
|
+
default: (value, defaultVal) => {
|
|
40
|
+
if (value === null || value === undefined || value === "") {
|
|
41
|
+
return defaultVal;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
},
|
|
45
|
+
isEmpty: (value) => {
|
|
46
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
47
|
+
if (value === null || value === undefined || value === "") return true;
|
|
48
|
+
return false;
|
|
49
|
+
},
|
|
50
|
+
date: (value, format = "YYYY-MM-DD") => {
|
|
51
|
+
if (!(value instanceof Date)) {
|
|
52
|
+
value = new Date(value);
|
|
53
|
+
}
|
|
54
|
+
if (isNaN(value.getTime())) return String(value);
|
|
55
|
+
|
|
56
|
+
const year = value.getFullYear();
|
|
57
|
+
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
58
|
+
const date = String(value.getDate()).padStart(2, "0");
|
|
59
|
+
const hours = String(value.getHours()).padStart(2, "0");
|
|
60
|
+
const minutes = String(value.getMinutes()).padStart(2, "0");
|
|
61
|
+
const seconds = String(value.getSeconds()).padStart(2, "0");
|
|
62
|
+
|
|
63
|
+
return (format as string)
|
|
64
|
+
.replace("YYYY", String(year))
|
|
65
|
+
.replace("MM", month)
|
|
66
|
+
.replace("DD", date)
|
|
67
|
+
.replace("HH", hours)
|
|
68
|
+
.replace("mm", minutes)
|
|
69
|
+
.replace("ss", seconds);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Safely get nested property from object
|
|
75
|
+
*/
|
|
76
|
+
function getNestedValue(obj: unknown, path: string): unknown {
|
|
77
|
+
const parts = path.split(".");
|
|
78
|
+
let current = obj;
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (current === null || current === undefined) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
current = (current as Record<string, unknown>)[part];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate simple conditions
|
|
92
|
+
* Supports: variable, !variable, var1 && var2, var1 || var2
|
|
93
|
+
*/
|
|
94
|
+
function evaluateCondition(condition: string, data: TemplateData): boolean {
|
|
95
|
+
condition = condition.trim();
|
|
96
|
+
|
|
97
|
+
// Handle logical operators (simple parsing, left-to-right)
|
|
98
|
+
if (condition.includes("||")) {
|
|
99
|
+
return condition
|
|
100
|
+
.split("||")
|
|
101
|
+
.map((c) => evaluateCondition(c.trim(), data))
|
|
102
|
+
.some((result) => result);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (condition.includes("&&")) {
|
|
106
|
+
return condition
|
|
107
|
+
.split("&&")
|
|
108
|
+
.map((c) => evaluateCondition(c.trim(), data))
|
|
109
|
+
.every((result) => result);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle negation
|
|
113
|
+
if (condition.startsWith("!")) {
|
|
114
|
+
return !evaluateCondition(condition.slice(1).trim(), data);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get variable value
|
|
118
|
+
const value = getNestedValue(data, condition);
|
|
119
|
+
|
|
120
|
+
// Truthy check
|
|
121
|
+
return Boolean(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Simple template renderer
|
|
126
|
+
*/
|
|
127
|
+
export class SimpleRenderer {
|
|
128
|
+
private filters: FilterRegistry;
|
|
129
|
+
|
|
130
|
+
constructor(customFilters?: FilterRegistry) {
|
|
131
|
+
this.filters = { ...builtinFilters, ...(customFilters || {}) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render template with data
|
|
136
|
+
*/
|
|
137
|
+
render(template: string, data: TemplateData): string {
|
|
138
|
+
let result = template;
|
|
139
|
+
|
|
140
|
+
// First, handle conditionals ({{ if ... }} ... {{ endif }})
|
|
141
|
+
result = this._processConditionals(result, data);
|
|
142
|
+
|
|
143
|
+
// Then, handle variables and filters ({{ var | filter1 | filter2 }})
|
|
144
|
+
result = this._processVariables(result, data);
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Process conditional blocks
|
|
151
|
+
*/
|
|
152
|
+
private _processConditionals(template: string, data: TemplateData): string {
|
|
153
|
+
// Match: {{ if condition }} ... {{ else }} ... {{ endif }}
|
|
154
|
+
// Handle else blocks properly
|
|
155
|
+
let result = template;
|
|
156
|
+
|
|
157
|
+
// Process nested if blocks (inside-out)
|
|
158
|
+
const ifRegex = /\{\{\s*if\s+([^}]+)\s*\}\}([\s\S]*?)\{\{\s*endif\s*\}\}/;
|
|
159
|
+
|
|
160
|
+
while (ifRegex.test(result)) {
|
|
161
|
+
result = result.replace(ifRegex, (match, condition, content) => {
|
|
162
|
+
// Check for else block
|
|
163
|
+
const elseRegex = /\{\{\s*else\s*\}\}([\s\S]*)$/;
|
|
164
|
+
let thenBlock = content;
|
|
165
|
+
let elseBlock = "";
|
|
166
|
+
|
|
167
|
+
const elseMatch = content.match(elseRegex);
|
|
168
|
+
if (elseMatch) {
|
|
169
|
+
thenBlock = content.substring(0, elseMatch.index);
|
|
170
|
+
elseBlock = elseMatch[1];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Evaluate condition
|
|
174
|
+
if (evaluateCondition(condition, data)) {
|
|
175
|
+
return thenBlock;
|
|
176
|
+
} else {
|
|
177
|
+
return elseBlock;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Process variable interpolation and filters
|
|
187
|
+
*/
|
|
188
|
+
private _processVariables(template: string, data: TemplateData): string {
|
|
189
|
+
// Match: {{ var | filter1(arg) | filter2 }}
|
|
190
|
+
const varRegex = /\{\{\s*([^|}\s][^}]*?)\s*(?:\|([^}]*?))?\s*\}\}/g;
|
|
191
|
+
|
|
192
|
+
return template.replace(varRegex, (match, varPath, filterChain) => {
|
|
193
|
+
// Get variable value
|
|
194
|
+
let value = getNestedValue(data, varPath.trim());
|
|
195
|
+
|
|
196
|
+
// If undefined, return empty string
|
|
197
|
+
if (value === undefined || value === null) {
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Apply filters if present
|
|
202
|
+
if (filterChain) {
|
|
203
|
+
const filters = filterChain.split("|").map((f) => f.trim());
|
|
204
|
+
|
|
205
|
+
for (const filterStr of filters) {
|
|
206
|
+
// Parse filter and arguments: "uppercase" or "slice(1, 5)"
|
|
207
|
+
const filterMatch = filterStr.match(/^(\w+)(?:\(([^)]*)\))?$/);
|
|
208
|
+
if (!filterMatch) continue;
|
|
209
|
+
|
|
210
|
+
const filterName = filterMatch[1];
|
|
211
|
+
const argsStr = filterMatch[2];
|
|
212
|
+
const filterFn = this.filters[filterName];
|
|
213
|
+
|
|
214
|
+
if (!filterFn) {
|
|
215
|
+
console.warn(`Unknown filter: ${filterName}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Parse arguments (simple string splitting)
|
|
220
|
+
const args: unknown[] = [];
|
|
221
|
+
if (argsStr) {
|
|
222
|
+
// Simple parsing: split by comma, handle quoted strings
|
|
223
|
+
let current = "";
|
|
224
|
+
let inQuotes = false;
|
|
225
|
+
let quoteChar = "";
|
|
226
|
+
for (const char of argsStr) {
|
|
227
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
228
|
+
inQuotes = true;
|
|
229
|
+
quoteChar = char;
|
|
230
|
+
} else if (char === quoteChar && inQuotes) {
|
|
231
|
+
inQuotes = false;
|
|
232
|
+
quoteChar = "";
|
|
233
|
+
} else if (char === "," && !inQuotes) {
|
|
234
|
+
const trimmed = current.trim();
|
|
235
|
+
// Remove quotes if present
|
|
236
|
+
const unquoted = trimmed.replace(/^["']|["']$/g, "");
|
|
237
|
+
args.push(unquoted);
|
|
238
|
+
current = "";
|
|
239
|
+
} else {
|
|
240
|
+
current += char;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (current) {
|
|
244
|
+
const trimmed = current.trim();
|
|
245
|
+
const unquoted = trimmed.replace(/^["']|["']$/g, "");
|
|
246
|
+
args.push(unquoted);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Apply filter
|
|
251
|
+
value = filterFn(value, ...args);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Convert to string
|
|
256
|
+
return String(value);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Register custom filter
|
|
262
|
+
*/
|
|
263
|
+
registerFilter(
|
|
264
|
+
name: string,
|
|
265
|
+
fn: (value: unknown, ...args: unknown[]) => unknown,
|
|
266
|
+
): void {
|
|
267
|
+
this.filters[name] = fn;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template System Types
|
|
3
|
+
*
|
|
4
|
+
* Core interfaces for the lightweight, multi-purpose template engine.
|
|
5
|
+
* Supports: Variables, filters, conditionals, Markdown rendering, and channel-specific variants.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Job status enumeration
|
|
10
|
+
*/
|
|
11
|
+
export type JobStatus =
|
|
12
|
+
| "pending"
|
|
13
|
+
| "processing"
|
|
14
|
+
| "completed"
|
|
15
|
+
| "failed"
|
|
16
|
+
| "delayed";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Template metadata from front matter
|
|
20
|
+
*/
|
|
21
|
+
export interface TemplateMetadata {
|
|
22
|
+
/** Channel variants supported by this template */
|
|
23
|
+
variants?: string[];
|
|
24
|
+
/** Default variant if not specified */
|
|
25
|
+
default?: string;
|
|
26
|
+
/** Template description */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Custom metadata */
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parsed template with content and metadata
|
|
34
|
+
*/
|
|
35
|
+
export interface Template {
|
|
36
|
+
/** Template identifier (path without extension) */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Template format: "markdown", "text", or "html" */
|
|
39
|
+
format: "markdown" | "text" | "html";
|
|
40
|
+
/** Raw template content */
|
|
41
|
+
content: string;
|
|
42
|
+
/** Channel-specific variants: { "email": "...", "sms": "...", "push": "..." } */
|
|
43
|
+
variants: Record<string, string>;
|
|
44
|
+
/** Front matter metadata */
|
|
45
|
+
metadata: TemplateMetadata;
|
|
46
|
+
/** When template was loaded (for cache invalidation) */
|
|
47
|
+
loadedAt: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for rendering a template
|
|
52
|
+
*/
|
|
53
|
+
export interface RenderOptions {
|
|
54
|
+
/** Specific variant to use (overrides auto-detection) */
|
|
55
|
+
variant?: string;
|
|
56
|
+
/** Output format: "html" for email, "text" for SMS/plain text */
|
|
57
|
+
outputFormat?: "html" | "text";
|
|
58
|
+
/** Timezone for date filters */
|
|
59
|
+
timezone?: string;
|
|
60
|
+
/** Locale for i18n (future) */
|
|
61
|
+
locale?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Template engine data (variables passed to renderer)
|
|
66
|
+
*/
|
|
67
|
+
export interface TemplateData {
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Renderer interface (implemented by SimpleRenderer, MarkdownRenderer, etc.)
|
|
73
|
+
*/
|
|
74
|
+
export interface IRenderer {
|
|
75
|
+
/** Render template with data */
|
|
76
|
+
render(template: string, data: TemplateData): Promise<string>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Filter function signature
|
|
81
|
+
*/
|
|
82
|
+
export type FilterFn = (value: unknown, ...args: unknown[]) => unknown;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Built-in filters registry
|
|
86
|
+
*/
|
|
87
|
+
export interface FilterRegistry {
|
|
88
|
+
[filterName: string]: FilterFn;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Template loader options
|
|
93
|
+
*/
|
|
94
|
+
export interface TemplateLoaderOptions {
|
|
95
|
+
/** Base path to templates directory */
|
|
96
|
+
basePath: string;
|
|
97
|
+
/** Enable caching */
|
|
98
|
+
cacheEnabled?: boolean;
|
|
99
|
+
/** Cache time-to-live in seconds */
|
|
100
|
+
cacheTtl?: number;
|
|
101
|
+
/** Max number of templates in cache */
|
|
102
|
+
maxCacheSize?: number;
|
|
103
|
+
/** Watch for file changes in development */
|
|
104
|
+
watch?: boolean;
|
|
105
|
+
/** File extension to look for */
|
|
106
|
+
extension?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Template engine configuration
|
|
111
|
+
*/
|
|
112
|
+
export interface TemplateEngineConfig {
|
|
113
|
+
/** Base path for templates */
|
|
114
|
+
basePath: string;
|
|
115
|
+
/** Cache configuration */
|
|
116
|
+
cache?: {
|
|
117
|
+
enabled: boolean;
|
|
118
|
+
ttl: number;
|
|
119
|
+
maxSize: number;
|
|
120
|
+
};
|
|
121
|
+
/** Watch for changes in development */
|
|
122
|
+
watch?: boolean;
|
|
123
|
+
/** Default locale (future i18n) */
|
|
124
|
+
locale?: string;
|
|
125
|
+
/** Default output format */
|
|
126
|
+
defaultFormat?: "html" | "text";
|
|
127
|
+
/** Channel to variant mapping for auto-detection */
|
|
128
|
+
channelVariantMap?: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Template load event
|
|
133
|
+
*/
|
|
134
|
+
export interface TemplateLoadedEvent {
|
|
135
|
+
templateId: string;
|
|
136
|
+
source: "memory" | "disk";
|
|
137
|
+
duration: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Template engine metrics
|
|
142
|
+
*/
|
|
143
|
+
export interface TemplateEngineMetrics {
|
|
144
|
+
/** Total templates loaded */
|
|
145
|
+
loaded: number;
|
|
146
|
+
/** Number of cache hits */
|
|
147
|
+
cacheHits: number;
|
|
148
|
+
/** Number of cache misses */
|
|
149
|
+
cacheMisses: number;
|
|
150
|
+
/** Average render time in milliseconds */
|
|
151
|
+
avgRenderTime: number;
|
|
152
|
+
/** Total renders */
|
|
153
|
+
totalRenders: number;
|
|
154
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -598,7 +598,9 @@ export class TestCache {
|
|
|
598
598
|
/**
|
|
599
599
|
* Create a new TestCache instance, optionally with initial data
|
|
600
600
|
*/
|
|
601
|
-
export async function createTestCache(
|
|
601
|
+
export async function createTestCache(
|
|
602
|
+
initialData?: Record<string, unknown>,
|
|
603
|
+
): Promise<TestCache> {
|
|
602
604
|
const cache = new TestCache();
|
|
603
605
|
if (initialData) {
|
|
604
606
|
await cache.setMany(initialData);
|
|
@@ -748,7 +750,10 @@ export class TestDatabase {
|
|
|
748
750
|
/**
|
|
749
751
|
* Execute a SQL query and return results
|
|
750
752
|
*/
|
|
751
|
-
async query<T = unknown>(
|
|
753
|
+
async query<T = unknown>(
|
|
754
|
+
sqlString: string,
|
|
755
|
+
params: unknown[] = [],
|
|
756
|
+
): Promise<T[]> {
|
|
752
757
|
this.ensureConnection();
|
|
753
758
|
|
|
754
759
|
this._operations.push({
|
|
@@ -768,7 +773,10 @@ export class TestDatabase {
|
|
|
768
773
|
/**
|
|
769
774
|
* Execute a query and return a single row
|
|
770
775
|
*/
|
|
771
|
-
async queryOne<T = unknown>(
|
|
776
|
+
async queryOne<T = unknown>(
|
|
777
|
+
sqlString: string,
|
|
778
|
+
params: unknown[] = [],
|
|
779
|
+
): Promise<T | null> {
|
|
772
780
|
const results = await this.query<T>(sqlString, params);
|
|
773
781
|
return results.length > 0 ? results[0] : null;
|
|
774
782
|
}
|
|
@@ -776,7 +784,14 @@ export class TestDatabase {
|
|
|
776
784
|
/**
|
|
777
785
|
* Execute a statement (INSERT, UPDATE, DELETE)
|
|
778
786
|
*/
|
|
779
|
-
async execute(
|
|
787
|
+
async execute(
|
|
788
|
+
sqlString: string,
|
|
789
|
+
params: unknown[] = [],
|
|
790
|
+
): Promise<{
|
|
791
|
+
rows: unknown[];
|
|
792
|
+
rowCount: number;
|
|
793
|
+
insertId?: number | string;
|
|
794
|
+
}> {
|
|
780
795
|
this.ensureConnection();
|
|
781
796
|
|
|
782
797
|
this._operations.push({
|
|
@@ -907,7 +922,10 @@ export class TestDatabase {
|
|
|
907
922
|
/**
|
|
908
923
|
* Create a table from column definitions
|
|
909
924
|
*/
|
|
910
|
-
async createTable(
|
|
925
|
+
async createTable(
|
|
926
|
+
name: string,
|
|
927
|
+
columns: Record<string, string>,
|
|
928
|
+
): Promise<void> {
|
|
911
929
|
const columnDefs = Object.entries(columns)
|
|
912
930
|
.map(([colName, def]) => `${colName} ${def}`)
|
|
913
931
|
.join(", ");
|
|
@@ -941,14 +959,27 @@ export class TestDatabase {
|
|
|
941
959
|
/**
|
|
942
960
|
* Get table info
|
|
943
961
|
*/
|
|
944
|
-
async getTableInfo(table: string): Promise<
|
|
962
|
+
async getTableInfo(table: string): Promise<
|
|
963
|
+
{
|
|
964
|
+
cid: number;
|
|
965
|
+
name: string;
|
|
966
|
+
type: string;
|
|
967
|
+
notnull: number;
|
|
968
|
+
dflt_value: unknown;
|
|
969
|
+
pk: number;
|
|
970
|
+
}[]
|
|
971
|
+
> {
|
|
945
972
|
return this.query(`PRAGMA table_info(${table})`);
|
|
946
973
|
}
|
|
947
974
|
|
|
948
975
|
/**
|
|
949
976
|
* Count rows in a table
|
|
950
977
|
*/
|
|
951
|
-
async count(
|
|
978
|
+
async count(
|
|
979
|
+
table: string,
|
|
980
|
+
where?: string,
|
|
981
|
+
params: unknown[] = [],
|
|
982
|
+
): Promise<number> {
|
|
952
983
|
const sql = where
|
|
953
984
|
? `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`
|
|
954
985
|
: `SELECT COUNT(*) as count FROM ${table}`;
|
|
@@ -960,7 +991,11 @@ export class TestDatabase {
|
|
|
960
991
|
/**
|
|
961
992
|
* Check if a row exists
|
|
962
993
|
*/
|
|
963
|
-
async exists(
|
|
994
|
+
async exists(
|
|
995
|
+
table: string,
|
|
996
|
+
where: string,
|
|
997
|
+
params: unknown[] = [],
|
|
998
|
+
): Promise<boolean> {
|
|
964
999
|
const count = await this.count(table, where, params);
|
|
965
1000
|
return count > 0;
|
|
966
1001
|
}
|
|
@@ -985,7 +1020,9 @@ export class TestDatabase {
|
|
|
985
1020
|
/**
|
|
986
1021
|
* Create a new TestDatabase instance, optionally with schema and seed data
|
|
987
1022
|
*/
|
|
988
|
-
export async function createTestDatabase(
|
|
1023
|
+
export async function createTestDatabase(
|
|
1024
|
+
options: TestDatabaseOptions = {},
|
|
1025
|
+
): Promise<TestDatabase> {
|
|
989
1026
|
const db = new TestDatabase();
|
|
990
1027
|
await db.connect();
|
|
991
1028
|
|
|
@@ -1059,7 +1096,10 @@ export async function assertTableNotHasRow(
|
|
|
1059
1096
|
/**
|
|
1060
1097
|
* Assert table exists in database
|
|
1061
1098
|
*/
|
|
1062
|
-
export async function assertTableExists(
|
|
1099
|
+
export async function assertTableExists(
|
|
1100
|
+
db: TestDatabase,
|
|
1101
|
+
table: string,
|
|
1102
|
+
): Promise<void> {
|
|
1063
1103
|
const tables = await db.getTables();
|
|
1064
1104
|
if (!tables.includes(table)) {
|
|
1065
1105
|
throw new Error(
|
|
@@ -1071,7 +1111,10 @@ export async function assertTableExists(db: TestDatabase, table: string): Promis
|
|
|
1071
1111
|
/**
|
|
1072
1112
|
* Assert table does not exist in database
|
|
1073
1113
|
*/
|
|
1074
|
-
export async function assertTableNotExists(
|
|
1114
|
+
export async function assertTableNotExists(
|
|
1115
|
+
db: TestDatabase,
|
|
1116
|
+
table: string,
|
|
1117
|
+
): Promise<void> {
|
|
1075
1118
|
const tables = await db.getTables();
|
|
1076
1119
|
if (tables.includes(table)) {
|
|
1077
1120
|
throw new Error(`Expected table "${table}" to NOT exist`);
|
|
@@ -1106,15 +1149,32 @@ export async function assertTableValue<T = unknown>(
|
|
|
1106
1149
|
}
|
|
1107
1150
|
// ============= Test Storage =============
|
|
1108
1151
|
|
|
1109
|
-
import {
|
|
1110
|
-
|
|
1152
|
+
import {
|
|
1153
|
+
copyFile,
|
|
1154
|
+
stat as fsStat,
|
|
1155
|
+
mkdir,
|
|
1156
|
+
readdir,
|
|
1157
|
+
rename,
|
|
1158
|
+
rm,
|
|
1159
|
+
unlink,
|
|
1160
|
+
} from "node:fs/promises";
|
|
1111
1161
|
import { tmpdir } from "node:os";
|
|
1162
|
+
import { join, relative, resolve } from "node:path";
|
|
1112
1163
|
|
|
1113
1164
|
/**
|
|
1114
1165
|
* Storage operation record for testing
|
|
1115
1166
|
*/
|
|
1116
1167
|
export interface StorageOperation {
|
|
1117
|
-
type:
|
|
1168
|
+
type:
|
|
1169
|
+
| "write"
|
|
1170
|
+
| "read"
|
|
1171
|
+
| "delete"
|
|
1172
|
+
| "exists"
|
|
1173
|
+
| "list"
|
|
1174
|
+
| "stat"
|
|
1175
|
+
| "copy"
|
|
1176
|
+
| "move"
|
|
1177
|
+
| "clear";
|
|
1118
1178
|
path?: string;
|
|
1119
1179
|
src?: string;
|
|
1120
1180
|
dest?: string;
|
|
@@ -1179,10 +1239,13 @@ export class TestStorage {
|
|
|
1179
1239
|
* @param path - Relative path within storage
|
|
1180
1240
|
* @param content - String or binary content
|
|
1181
1241
|
*/
|
|
1182
|
-
async write(
|
|
1242
|
+
async write(
|
|
1243
|
+
path: string,
|
|
1244
|
+
content: string | Uint8Array | ArrayBuffer,
|
|
1245
|
+
): Promise<void> {
|
|
1183
1246
|
await this.ensureInitialized();
|
|
1184
1247
|
const fullPath = this.resolvePath(path);
|
|
1185
|
-
|
|
1248
|
+
|
|
1186
1249
|
// Ensure parent directory exists
|
|
1187
1250
|
const parentDir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
1188
1251
|
if (parentDir) {
|
|
@@ -1192,7 +1255,7 @@ export class TestStorage {
|
|
|
1192
1255
|
// Write content using Bun.file()
|
|
1193
1256
|
const file = Bun.file(fullPath);
|
|
1194
1257
|
const writer = file.writer();
|
|
1195
|
-
|
|
1258
|
+
|
|
1196
1259
|
if (typeof content === "string") {
|
|
1197
1260
|
writer.write(content);
|
|
1198
1261
|
} else if (content instanceof Uint8Array) {
|
|
@@ -1200,12 +1263,13 @@ export class TestStorage {
|
|
|
1200
1263
|
} else if (content instanceof ArrayBuffer) {
|
|
1201
1264
|
writer.write(new Uint8Array(content));
|
|
1202
1265
|
}
|
|
1203
|
-
|
|
1266
|
+
|
|
1204
1267
|
await writer.end();
|
|
1205
1268
|
|
|
1206
|
-
const size =
|
|
1207
|
-
|
|
1208
|
-
|
|
1269
|
+
const size =
|
|
1270
|
+
typeof content === "string"
|
|
1271
|
+
? new TextEncoder().encode(content).length
|
|
1272
|
+
: content.byteLength;
|
|
1209
1273
|
|
|
1210
1274
|
this._operations.push({
|
|
1211
1275
|
type: "write",
|
|
@@ -1437,7 +1501,7 @@ export class TestStorage {
|
|
|
1437
1501
|
*/
|
|
1438
1502
|
async clear(): Promise<void> {
|
|
1439
1503
|
await this.ensureInitialized();
|
|
1440
|
-
|
|
1504
|
+
|
|
1441
1505
|
try {
|
|
1442
1506
|
const files = await this.list();
|
|
1443
1507
|
for (const file of files) {
|
|
@@ -1505,8 +1569,11 @@ export class TestStorage {
|
|
|
1505
1569
|
* Create a new TestStorage instance
|
|
1506
1570
|
* @param options - Optional configuration
|
|
1507
1571
|
*/
|
|
1508
|
-
export async function createTestStorage(
|
|
1509
|
-
|
|
1572
|
+
export async function createTestStorage(
|
|
1573
|
+
options: TestStorageOptions = {},
|
|
1574
|
+
): Promise<TestStorage> {
|
|
1575
|
+
const basePath =
|
|
1576
|
+
options.basePath ?? (await createTempDir("bueno-test-storage-"));
|
|
1510
1577
|
const storage = new TestStorage(basePath);
|
|
1511
1578
|
await storage.init();
|
|
1512
1579
|
return storage;
|
|
@@ -1527,7 +1594,10 @@ async function createTempDir(prefix: string): Promise<string> {
|
|
|
1527
1594
|
/**
|
|
1528
1595
|
* Assert that a file exists in storage
|
|
1529
1596
|
*/
|
|
1530
|
-
export async function assertFileExists(
|
|
1597
|
+
export async function assertFileExists(
|
|
1598
|
+
storage: TestStorage,
|
|
1599
|
+
path: string,
|
|
1600
|
+
): Promise<void> {
|
|
1531
1601
|
const exists = await storage.exists(path);
|
|
1532
1602
|
if (!exists) {
|
|
1533
1603
|
const files = await storage.list();
|
|
@@ -1540,7 +1610,10 @@ export async function assertFileExists(storage: TestStorage, path: string): Prom
|
|
|
1540
1610
|
/**
|
|
1541
1611
|
* Assert that a file does not exist in storage
|
|
1542
1612
|
*/
|
|
1543
|
-
export async function assertFileNotExists(
|
|
1613
|
+
export async function assertFileNotExists(
|
|
1614
|
+
storage: TestStorage,
|
|
1615
|
+
path: string,
|
|
1616
|
+
): Promise<void> {
|
|
1544
1617
|
const exists = await storage.exists(path);
|
|
1545
1618
|
if (exists) {
|
|
1546
1619
|
throw new Error(`Expected file "${path}" to NOT exist`);
|
|
@@ -1583,4 +1656,4 @@ export async function assertFileSize(
|
|
|
1583
1656
|
`Expected file "${path}" to have size ${expectedSize}, got ${stats.size}`,
|
|
1584
1657
|
);
|
|
1585
1658
|
}
|
|
1586
|
-
}
|
|
1659
|
+
}
|