@b9g/zen 0.1.1 → 0.1.3

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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Structured error types for database operations.
3
+ *
4
+ * All database errors extend DatabaseError, which includes an error code
5
+ * for programmatic error handling.
6
+ */
7
+ export type DatabaseErrorCode = "VALIDATION_ERROR" | "TABLE_DEFINITION_ERROR" | "MIGRATION_ERROR" | "MIGRATION_LOCK_ERROR" | "QUERY_ERROR" | "NOT_FOUND" | "ALREADY_EXISTS" | "CONSTRAINT_VIOLATION" | "CONNECTION_ERROR" | "TRANSACTION_ERROR" | "ENSURE_ERROR" | "SCHEMA_DRIFT_ERROR" | "CONSTRAINT_PREFLIGHT_ERROR";
8
+ /**
9
+ * Base error class for all database errors.
10
+ *
11
+ * Includes an error code for programmatic handling.
12
+ */
13
+ export declare class DatabaseError extends Error {
14
+ readonly code: DatabaseErrorCode;
15
+ constructor(code: DatabaseErrorCode, message: string, options?: ErrorOptions);
16
+ }
17
+ /**
18
+ * Thrown when Zod validation fails during insert/update.
19
+ */
20
+ export declare class ValidationError extends DatabaseError {
21
+ readonly fieldErrors: Record<string, string[]>;
22
+ constructor(message: string, fieldErrors?: Record<string, string[]>, options?: ErrorOptions);
23
+ }
24
+ /**
25
+ * Thrown when table definition is invalid (e.g., dots in names).
26
+ */
27
+ export declare class TableDefinitionError extends DatabaseError {
28
+ readonly tableName?: string;
29
+ readonly fieldName?: string;
30
+ constructor(message: string, tableName?: string, fieldName?: string, options?: ErrorOptions);
31
+ }
32
+ /**
33
+ * Thrown when migration fails.
34
+ */
35
+ export declare class MigrationError extends DatabaseError {
36
+ readonly fromVersion: number;
37
+ readonly toVersion: number;
38
+ constructor(message: string, fromVersion: number, toVersion: number, options?: ErrorOptions);
39
+ }
40
+ /**
41
+ * Thrown when migration lock cannot be acquired.
42
+ */
43
+ export declare class MigrationLockError extends DatabaseError {
44
+ constructor(message: string, options?: ErrorOptions);
45
+ }
46
+ /**
47
+ * Thrown when a query fails.
48
+ */
49
+ export declare class QueryError extends DatabaseError {
50
+ readonly sql?: string;
51
+ constructor(message: string, sql?: string, options?: ErrorOptions);
52
+ }
53
+ /**
54
+ * Thrown when an expected entity is not found.
55
+ */
56
+ export declare class NotFoundError extends DatabaseError {
57
+ readonly tableName: string;
58
+ readonly id?: unknown;
59
+ constructor(tableName: string, id?: unknown, options?: ErrorOptions);
60
+ }
61
+ /**
62
+ * Thrown when trying to create an entity that already exists.
63
+ */
64
+ export declare class AlreadyExistsError extends DatabaseError {
65
+ readonly tableName: string;
66
+ readonly field?: string;
67
+ readonly value?: unknown;
68
+ constructor(tableName: string, field?: string, value?: unknown, options?: ErrorOptions);
69
+ }
70
+ /**
71
+ * Thrown when a database constraint is violated.
72
+ *
73
+ * Constraint violations are detected at the database level and converted
74
+ * from driver-specific errors into this normalized format.
75
+ *
76
+ * **Stable fields**: All fields are always present with defined types.
77
+ * Best-effort extraction from driver errors means some may be undefined.
78
+ *
79
+ * **Transaction behavior**: This error is thrown immediately and does NOT
80
+ * auto-rollback. The caller or driver transaction wrapper handles rollback.
81
+ */
82
+ export declare class ConstraintViolationError extends DatabaseError {
83
+ /**
84
+ * Type of constraint that was violated.
85
+ * "unknown" if the specific type couldn't be determined from the error.
86
+ */
87
+ readonly kind: "unique" | "foreign_key" | "check" | "not_null" | "unknown";
88
+ /**
89
+ * Name of the constraint (e.g., "users_email_unique", "users.email").
90
+ * May be undefined if the database error didn't include it.
91
+ */
92
+ readonly constraint?: string;
93
+ /**
94
+ * Table name where the violation occurred.
95
+ * May be undefined if not extractable from the error.
96
+ */
97
+ readonly table?: string;
98
+ /**
99
+ * Column name involved in the violation.
100
+ * May be undefined if not extractable from the error.
101
+ */
102
+ readonly column?: string;
103
+ constructor(message: string, details: {
104
+ kind: "unique" | "foreign_key" | "check" | "not_null" | "unknown";
105
+ constraint?: string;
106
+ table?: string;
107
+ column?: string;
108
+ }, options?: ErrorOptions);
109
+ }
110
+ /**
111
+ * Thrown when database connection fails.
112
+ */
113
+ export declare class ConnectionError extends DatabaseError {
114
+ constructor(message: string, options?: ErrorOptions);
115
+ }
116
+ /**
117
+ * Thrown when a transaction fails.
118
+ */
119
+ export declare class TransactionError extends DatabaseError {
120
+ constructor(message: string, options?: ErrorOptions);
121
+ }
122
+ /**
123
+ * Operation type for ensure operations.
124
+ */
125
+ export type EnsureOperation = "ensureTable" | "ensureConstraints" | "copyColumn";
126
+ /**
127
+ * Thrown when an ensure operation fails.
128
+ *
129
+ * Includes step information for diagnosing partial failures,
130
+ * since DDL is not reliably transactional on all databases (especially MySQL).
131
+ */
132
+ export declare class EnsureError extends DatabaseError {
133
+ /** The operation that failed */
134
+ readonly operation: EnsureOperation;
135
+ /** The table being operated on */
136
+ readonly table: string;
137
+ /** The step index where failure occurred (0-based) */
138
+ readonly step: number;
139
+ constructor(message: string, details: {
140
+ operation: EnsureOperation;
141
+ table: string;
142
+ step: number;
143
+ }, options?: ErrorOptions);
144
+ }
145
+ /**
146
+ * Thrown when schema drift is detected.
147
+ *
148
+ * Schema drift occurs when an existing database object doesn't match
149
+ * the expected schema definition. For example, a column exists but
150
+ * has a different type, or an index covers different columns.
151
+ */
152
+ export declare class SchemaDriftError extends DatabaseError {
153
+ /** The table where drift was detected */
154
+ readonly table: string;
155
+ /** Description of what drifted */
156
+ readonly drift: string;
157
+ /** Suggested action to resolve */
158
+ readonly suggestion?: string;
159
+ constructor(message: string, details: {
160
+ table: string;
161
+ drift: string;
162
+ suggestion?: string;
163
+ }, options?: ErrorOptions);
164
+ }
165
+ /**
166
+ * Thrown when a constraint preflight check finds violations.
167
+ *
168
+ * Before adding a UNIQUE constraint or foreign key to existing data,
169
+ * a preflight check verifies data integrity. This error is thrown
170
+ * when violations are found, including the query used to detect them.
171
+ */
172
+ export declare class ConstraintPreflightError extends DatabaseError {
173
+ /** The table being constrained */
174
+ readonly table: string;
175
+ /** The constraint being added (e.g., "unique:email" or "fk:authorId") */
176
+ readonly constraint: string;
177
+ /** Number of violating rows */
178
+ readonly violationCount: number;
179
+ /** The SQL query that found the violations - run it to see details */
180
+ readonly query: string;
181
+ constructor(message: string, details: {
182
+ table: string;
183
+ constraint: string;
184
+ violationCount: number;
185
+ query: string;
186
+ }, options?: ErrorOptions);
187
+ }
188
+ /**
189
+ * Check if an error is a DatabaseError.
190
+ */
191
+ export declare function isDatabaseError(error: unknown): error is DatabaseError;
192
+ /**
193
+ * Check if an error has a specific error code.
194
+ */
195
+ export declare function hasErrorCode(error: unknown, code: DatabaseErrorCode): error is DatabaseError;
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Query layer - tagged template SQL with parameterized queries.
3
+ *
4
+ * Generates SELECT statements with prefixed column aliases for entity normalization.
5
+ */
6
+ import { type Table, type DriverDecoder } from "./table.js";
7
+ import { type SQLTemplate } from "./template.js";
8
+ import { type SQLDialect } from "./sql.js";
9
+ export type { SQLDialect } from "./sql.js";
10
+ export interface QueryOptions {
11
+ dialect?: SQLDialect;
12
+ }
13
+ export interface ParsedQuery {
14
+ sql: string;
15
+ params: unknown[];
16
+ }
17
+ /**
18
+ * Render a SQL template to {sql, params} format.
19
+ * Useful for testing and debugging template output.
20
+ *
21
+ * @param template - SQLTemplate tuple [strings, ...values]
22
+ * @param dialect - SQL dialect for identifier quoting (default: sqlite)
23
+ * @returns {sql, params} for the rendered template
24
+ */
25
+ export declare function renderFragment(template: SQLTemplate, dialect?: SQLDialect): {
26
+ sql: string;
27
+ params: unknown[];
28
+ };
29
+ /**
30
+ * Build SELECT clause as a template with ident markers.
31
+ *
32
+ * Returns a template tuple that can be rendered with renderSQL().
33
+ * All identifiers use ident() markers instead of dialect-specific quoting.
34
+ *
35
+ * @example
36
+ * const {strings, values} = buildSelectColumnsTemplate([posts, users]);
37
+ * // values contains ident markers: [ident("posts"), ident("id"), ident("posts.id"), ...]
38
+ */
39
+ export declare function buildSelectColumnsTemplate(tables: Table<any>[]): {
40
+ strings: TemplateStringsArray;
41
+ values: unknown[];
42
+ };
43
+ /**
44
+ * Build SELECT clause with prefixed column aliases.
45
+ *
46
+ * For derived tables (created via Table.derive()), this function:
47
+ * - Skips derived fields when outputting regular columns
48
+ * - Appends derived SQL expressions with auto-generated aliases
49
+ *
50
+ * @example
51
+ * buildSelectColumns([posts, users], "sqlite")
52
+ * // { sql: '"posts"."id" AS "posts.id", ...', params: [] }
53
+ *
54
+ * @example
55
+ * // With derived table
56
+ * const PostsWithCount = Posts.derive('likeCount', z.number())`COUNT(*)`;
57
+ * buildSelectColumns([PostsWithCount], "sqlite")
58
+ * // { sql: '"posts"."id" AS "posts.id", ..., COUNT(*) AS "posts.likeCount"', params: [] }
59
+ */
60
+ export declare function buildSelectColumns(tables: Table<any>[], dialect?: SQLDialect): {
61
+ sql: string;
62
+ params: unknown[];
63
+ };
64
+ /**
65
+ * Expand a template by flattening nested fragments and converting tables to ident markers.
66
+ *
67
+ * This produces a flat template tuple with:
68
+ * - SQLTemplate tuples merged in
69
+ * - Table objects converted to ident() markers
70
+ * - Regular values passed through
71
+ *
72
+ * The result can be rendered with renderSQL() for final SQL output.
73
+ *
74
+ * @param strings - Template strings
75
+ * @param values - Template values
76
+ * @returns Flat template tuple {strings, values}
77
+ */
78
+ export declare function expandTemplate(strings: TemplateStringsArray, values: unknown[]): {
79
+ strings: TemplateStringsArray;
80
+ values: unknown[];
81
+ };
82
+ /**
83
+ * Parse a tagged template into SQL string and params array.
84
+ *
85
+ * Supports:
86
+ * - SQL templates/fragments: merged and parameterized
87
+ * - Table objects: interpolated as quoted table names
88
+ * - Other values: become parameterized placeholders
89
+ *
90
+ * @example
91
+ * parseTemplate`WHERE id = ${userId} AND active = ${true}`
92
+ * // { sql: "WHERE id = ? AND active = ?", params: ["user-123", true] }
93
+ *
94
+ * @example
95
+ * parseTemplate`WHERE ${where(Users, { role: "admin" })}`
96
+ * // { sql: "WHERE role = ?", params: ["admin"] }
97
+ *
98
+ * @example
99
+ * parseTemplate`FROM ${Posts} JOIN ${Users} ON ...`
100
+ * // { sql: 'FROM "posts" JOIN "users" ON ...', params: [] }
101
+ */
102
+ export declare function parseTemplate(strings: TemplateStringsArray, values: unknown[], dialect?: SQLDialect): ParsedQuery;
103
+ /**
104
+ * Build a full SELECT query as a template with ident markers.
105
+ *
106
+ * Returns a template tuple that can be rendered with renderSQL().
107
+ * The userClauses string is appended as-is (it should already contain placeholders).
108
+ *
109
+ * @example
110
+ * const {strings, values} = buildQueryTemplate([posts, users], "WHERE published = ?");
111
+ * // values contains ident markers plus any params from derived expressions
112
+ */
113
+ /**
114
+ * @internal
115
+ * Build a query template from tables and raw SQL clauses.
116
+ *
117
+ * WARNING: userClauses is appended verbatim as raw SQL. Do not pass untrusted input.
118
+ * For parameterized queries, use Database methods or tagged templates instead.
119
+ */
120
+ export declare function buildQueryTemplate(tables: Table<any>[], userClauses?: string): {
121
+ strings: TemplateStringsArray;
122
+ values: unknown[];
123
+ };
124
+ /**
125
+ * @internal
126
+ * Build a full SELECT query for tables with user-provided clauses.
127
+ *
128
+ * WARNING: userClauses is appended verbatim as raw SQL. Do not pass untrusted input.
129
+ * For parameterized queries, use Database methods or tagged templates instead.
130
+ *
131
+ * @example
132
+ * buildQuery([posts, users], "JOIN users ON users.id = posts.author_id WHERE published = ?", "sqlite")
133
+ */
134
+ export declare function buildQuery(tables: Table<any>[], userClauses: string, dialect?: SQLDialect): {
135
+ sql: string;
136
+ params: unknown[];
137
+ };
138
+ /**
139
+ * Create a tagged template function for querying tables.
140
+ *
141
+ * @example
142
+ * const query = createQuery([posts, users], "sqlite");
143
+ * const { sql, params } = query`
144
+ * JOIN users ON users.id = posts.author_id
145
+ * WHERE published = ${true}
146
+ * `;
147
+ */
148
+ export declare function createQuery(tables: Table<any>[], dialect?: SQLDialect): (strings: TemplateStringsArray, ...values: unknown[]) => ParsedQuery;
149
+ /**
150
+ * Parse a raw SQL template (no table-based SELECT generation).
151
+ *
152
+ * @example
153
+ * const { sql, params } = rawQuery`SELECT COUNT(*) FROM posts WHERE author_id = ${userId}`;
154
+ */
155
+ export declare function rawQuery(strings: TemplateStringsArray, ...values: unknown[]): ParsedQuery;
156
+ /**
157
+ * Create a raw query function for a specific dialect.
158
+ */
159
+ export declare function createRawQuery(dialect: SQLDialect): (strings: TemplateStringsArray, ...values: unknown[]) => ParsedQuery;
160
+ /**
161
+ * Entity normalization - Apollo-style entity deduplication with reference resolution.
162
+ *
163
+ * Takes raw SQL results with prefixed columns and returns normalized entities
164
+ * with references resolved to actual object instances.
165
+ */
166
+ /**
167
+ * Raw row from SQL query with prefixed column names.
168
+ * @example { "posts.id": "p1", "posts.authorId": "u1", "users.id": "u1", "users.name": "Alice" }
169
+ */
170
+ export type RawRow = Record<string, unknown>;
171
+ /**
172
+ * Entity map keyed by "table:primaryKey"
173
+ */
174
+ export type EntityMap = Map<string, Record<string, unknown>>;
175
+ /**
176
+ * Table map by table name for lookup
177
+ */
178
+ export type TableMap = Map<string, Table<any>>;
179
+ /**
180
+ * Extract entity data from a raw row for a specific table.
181
+ *
182
+ * @example
183
+ * extractEntityData({ "posts.id": "p1", "users.id": "u1" }, "posts")
184
+ * // { id: "p1" }
185
+ */
186
+ export declare function extractEntityData(row: RawRow, tableName: string): Record<string, unknown> | null;
187
+ /**
188
+ * Get the primary key value for an entity.
189
+ */
190
+ export declare function getPrimaryKeyValue(entity: Record<string, unknown>, table: Table<any>): string | null;
191
+ /**
192
+ * Create entity key for the entity map.
193
+ */
194
+ export declare function entityKey(tableName: string, primaryKey: string): string;
195
+ /**
196
+ * Build an entity map from raw rows.
197
+ *
198
+ * Entities are deduplicated - same primary key = same object instance.
199
+ * Each entity is parsed through its table's schema for type coercion
200
+ * (e.g., z.coerce.date() converts date strings to Date objects).
201
+ */
202
+ export declare function buildEntityMap(rows: RawRow[], tables: Table<any>[], driver?: DriverDecoder): EntityMap;
203
+ /**
204
+ * Resolve references for all entities in the map.
205
+ *
206
+ * Resolves both forward references (belongs-to) and reverse references (has-many).
207
+ *
208
+ * **Forward references** (`as`): Populates referenced entity as a single object.
209
+ * **Reverse references** (`reverseAs`): Populates array of referencing entities.
210
+ *
211
+ * **Performance**: Uses indexing to avoid O(n²) scans - builds index once per table,
212
+ * then attaches in O(n).
213
+ *
214
+ * **Ordering**: Reverse relationship arrays follow query result order, which is
215
+ * database-dependent unless you specify ORDER BY in your SQL.
216
+ */
217
+ export declare function resolveReferences(entities: EntityMap, tables: Table<any>[]): void;
218
+ /**
219
+ * Apply derived properties to entities.
220
+ *
221
+ * Derived properties are non-enumerable lazy getters that transform already-fetched data.
222
+ * They must be pure functions (no I/O, no side effects).
223
+ */
224
+ export declare function applyDerivedProperties(entities: EntityMap, tables: Table<any>[]): void;
225
+ /**
226
+ * Extract main table entities from the entity map in row order.
227
+ *
228
+ * Maintains the order from the original query results.
229
+ */
230
+ export declare function extractMainEntities<T>(rows: RawRow[], mainTable: Table<any>, entities: EntityMap): T[];
231
+ /**
232
+ * Normalize raw SQL rows into deduplicated entities with resolved references.
233
+ *
234
+ * @example
235
+ * const rows = [
236
+ * { "posts.id": "p1", "posts.authorId": "u1", "users.id": "u1", "users.name": "Alice" },
237
+ * { "posts.id": "p2", "posts.authorId": "u1", "users.id": "u1", "users.name": "Alice" },
238
+ * ];
239
+ *
240
+ * const posts = normalize(rows, [posts, users]);
241
+ * // posts[0].author === posts[1].author // Same instance!
242
+ */
243
+ export declare function normalize<T>(rows: RawRow[], tables: Table<any>[], driver?: DriverDecoder): T[];
244
+ /**
245
+ * Normalize a single row into an entity.
246
+ *
247
+ * Returns null if the main table has no data (e.g., no match).
248
+ */
249
+ export declare function normalizeOne<T>(row: RawRow | null, tables: Table<any>[], driver?: DriverDecoder): T | null;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SQL rendering utilities for all dialects.
3
+ *
4
+ * This is the single source of truth for dialect-specific SQL rendering:
5
+ * - Identifier quoting
6
+ * - Placeholder syntax
7
+ * - SQL builtin resolution
8
+ * - Template rendering (for DDL and queries)
9
+ */
10
+ export type SQLDialect = "sqlite" | "postgresql" | "mysql";
11
+ /**
12
+ * Quote an identifier based on dialect.
13
+ * MySQL uses backticks, PostgreSQL/SQLite use double quotes.
14
+ */
15
+ export declare function quoteIdent(name: string, dialect: SQLDialect): string;
16
+ /**
17
+ * Get placeholder syntax based on dialect.
18
+ * PostgreSQL uses $1, $2, etc. MySQL/SQLite use ?.
19
+ */
20
+ export declare function placeholder(index: number, dialect: SQLDialect): string;
21
+ export { resolveSQLBuiltin } from "./builtins.js";
22
+ /**
23
+ * Render a template to SQL string with parameters.
24
+ * Handles SQLIdentifier markers and regular values.
25
+ */
26
+ export declare function renderSQL(strings: TemplateStringsArray, values: readonly unknown[], dialect: SQLDialect): {
27
+ sql: string;
28
+ params: unknown[];
29
+ };
30
+ /**
31
+ * Render a DDL template to SQL string.
32
+ * Handles identifiers (quoted), SQL builtins (resolved), and literal values (escaped/inlined).
33
+ * Used for CREATE TABLE, CREATE VIEW, etc.
34
+ */
35
+ export declare function renderDDL(strings: TemplateStringsArray, values: readonly unknown[], dialect: SQLDialect): string;
36
+ /**
37
+ * Build SQL from template parts with parameter placeholders.
38
+ *
39
+ * This is the shared implementation used by all Node drivers (MySQL, PostgreSQL, SQLite).
40
+ * Handles SQLBuiltin symbols, SQLIdentifiers, and regular parameter values.
41
+ *
42
+ * SQL builtins and identifiers are inlined directly; other values use placeholders.
43
+ */
44
+ export declare function buildSQL(strings: TemplateStringsArray, values: unknown[], dialect: SQLDialect): {
45
+ sql: string;
46
+ params: unknown[];
47
+ };