@kitledger/core 0.0.1 → 0.0.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.
- package/dist/accounts.d.ts +44 -0
- package/dist/accounts.js +221 -0
- package/dist/auth.d.ts +34 -0
- package/dist/auth.js +24 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +48 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +13 -0
- package/dist/db.d.ts +78 -0
- package/dist/db.js +86 -0
- package/dist/factories.d.ts +45 -0
- package/dist/factories.js +150 -0
- package/dist/fields.d.ts +71 -0
- package/dist/fields.js +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/jwt.d.ts +10 -0
- package/dist/jwt.js +66 -0
- package/dist/ledgers.d.ts +22 -0
- package/dist/ledgers.js +144 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.js +217 -0
- package/dist/schema.d.ts +1190 -0
- package/dist/schema.js +159 -0
- package/dist/transactions.d.ts +22 -2
- package/dist/transactions.js +5 -1
- package/dist/ui.d.ts +3 -2
- package/dist/users.d.ts +16 -0
- package/dist/users.js +198 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.js +10 -0
- package/package.json +22 -8
package/dist/query.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { QuerySchema } from "@kitledger/query";
|
|
2
|
+
import { getTableName } from "drizzle-orm";
|
|
3
|
+
import knex from "knex";
|
|
4
|
+
import * as v from "valibot";
|
|
5
|
+
import { defaultLimit, defaultOffset, maxLimit, QueryResultSchema, } from "./db.js";
|
|
6
|
+
import { parseValibotIssues } from "./validation.js";
|
|
7
|
+
/**
|
|
8
|
+
* Maximum allowed nesting depth for filter groups to prevent overly complex queries.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_NESTING_DEPTH = 5;
|
|
11
|
+
/**
|
|
12
|
+
* Use valibot to validate and parse the incoming query parameters.
|
|
13
|
+
* @param params
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
function validateQueryParams(params) {
|
|
17
|
+
const result = v.safeParse(QuerySchema, params);
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
return { success: false, errors: parseValibotIssues(result.issues) };
|
|
20
|
+
}
|
|
21
|
+
return { success: true, data: result.output };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Prepares and executes a query against the specified table using the provided parameters.
|
|
25
|
+
* @param table
|
|
26
|
+
* @param params
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
export async function executeQuery(db, table, params) {
|
|
30
|
+
const validationResult = validateQueryParams(params);
|
|
31
|
+
const parsedParams = validationResult.success ? validationResult.data : null;
|
|
32
|
+
if (!validationResult.success || !parsedParams) {
|
|
33
|
+
console.error("Validation errors", validationResult.errors);
|
|
34
|
+
return {
|
|
35
|
+
data: [],
|
|
36
|
+
count: 0,
|
|
37
|
+
offset: 0,
|
|
38
|
+
limit: 0,
|
|
39
|
+
errors: validationResult.errors?.map((e) => ({ field: e.path || undefined, message: e.message })),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const knexBuilder = knex({ client: "pg" });
|
|
44
|
+
const limit = Math.min(parsedParams.limit ?? defaultLimit, maxLimit);
|
|
45
|
+
const offset = parsedParams.offset ?? defaultOffset;
|
|
46
|
+
const { sql, bindings } = buildQuery(knexBuilder, getTableName(table), params, limit, offset)
|
|
47
|
+
.toSQL()
|
|
48
|
+
.toNative();
|
|
49
|
+
console.log("Executing query:", sql, bindings);
|
|
50
|
+
const queryResult = await db.$client.unsafe(sql, bindings);
|
|
51
|
+
console.log("Query result:", queryResult);
|
|
52
|
+
const parsedQueryResult = v.safeParse(QueryResultSchema, queryResult);
|
|
53
|
+
if (!parsedQueryResult.success) {
|
|
54
|
+
console.error("Failed to parse query result", parsedQueryResult.issues);
|
|
55
|
+
throw new Error("Failed to parse query result");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
data: parsedQueryResult.output,
|
|
59
|
+
count: parsedQueryResult.output.length ?? 0,
|
|
60
|
+
offset: offset,
|
|
61
|
+
limit: limit,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
data: [],
|
|
67
|
+
count: 0,
|
|
68
|
+
offset: 0,
|
|
69
|
+
limit: 0,
|
|
70
|
+
errors: [{ message: error instanceof Error ? error.message : "Query execution error" }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Recursively applies filters from a ConditionGroup to a Knex query builder.
|
|
76
|
+
* @param queryBuilder
|
|
77
|
+
* @param filterGroup
|
|
78
|
+
* @param depth
|
|
79
|
+
*/
|
|
80
|
+
function applyFilters(queryBuilder, filterGroup, depth) {
|
|
81
|
+
if (depth > MAX_NESTING_DEPTH) {
|
|
82
|
+
throw new Error(`Query nesting depth exceeds the maximum of ${MAX_NESTING_DEPTH}.`);
|
|
83
|
+
}
|
|
84
|
+
// Use a nested 'where' to group conditions with parentheses, e.g., WHERE ( ... )
|
|
85
|
+
queryBuilder.where(function () {
|
|
86
|
+
for (const filter of filterGroup.conditions) {
|
|
87
|
+
// Determine the chaining method (.where or .orWhere)
|
|
88
|
+
const connector = filterGroup.connector;
|
|
89
|
+
const method = filterGroup.connector === "or" ? "orWhere" : "where";
|
|
90
|
+
// If the filter is another group, recurse
|
|
91
|
+
if ("connector" in filter) {
|
|
92
|
+
this[method](function () {
|
|
93
|
+
// Pass the nested group directly and increment the depth
|
|
94
|
+
applyFilters(this, filter, depth + 1);
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Apply the specific filter condition
|
|
99
|
+
const { column, operator, value } = filter;
|
|
100
|
+
switch (operator) {
|
|
101
|
+
case "in": {
|
|
102
|
+
const caseMethod = connector === "or" ? "orWhereIn" : "whereIn";
|
|
103
|
+
this[caseMethod](column, value);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "not_in": {
|
|
107
|
+
const caseMethod = connector === "or" ? "orWhereNotIn" : "whereNotIn";
|
|
108
|
+
this[caseMethod](column, value);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "empty": {
|
|
112
|
+
const caseMethod = connector === "or" ? "orWhereNull" : "whereNull";
|
|
113
|
+
this[caseMethod](column);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "not_empty": {
|
|
117
|
+
const caseMethod = connector === "or" ? "orWhereNotNull" : "whereNotNull";
|
|
118
|
+
this[caseMethod](column);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
// Handles =, !=, >, <, etc.
|
|
122
|
+
default: {
|
|
123
|
+
this[method](column, operator, value);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Builds a Knex query object from a QueryOptions configuration.
|
|
132
|
+
* @param kx - The Knex instance.
|
|
133
|
+
* @param tableName - The name of the table to query.
|
|
134
|
+
* @param options - The QueryOptions object.
|
|
135
|
+
* @returns A Knex QueryBuilder instance.
|
|
136
|
+
*/
|
|
137
|
+
export function buildQuery(kx, tableName, options, limit, offset) {
|
|
138
|
+
let query;
|
|
139
|
+
let fromClause = tableName;
|
|
140
|
+
// 1. Process Recursive CTE (now with hardcoded conventions)
|
|
141
|
+
if (options.recursive) {
|
|
142
|
+
// Hardcoded conventions for simplicity
|
|
143
|
+
const cteName = "hierarchy";
|
|
144
|
+
const parentKey = "id";
|
|
145
|
+
const childKey = "parent_id";
|
|
146
|
+
fromClause = cteName; // The rest of the query will select FROM the CTE result.
|
|
147
|
+
const { direction, startWith } = options.recursive;
|
|
148
|
+
// Build the "anchor" query that finds the starting records.
|
|
149
|
+
const anchorBuilder = kx.from(tableName).where((qb) => applyFilters(qb, startWith, 1));
|
|
150
|
+
const { sql: anchorSql, bindings: anchorBindings } = anchorBuilder.toSQL().toNative();
|
|
151
|
+
// Determine the join direction based on 'ancestors' or 'descendants'
|
|
152
|
+
const [joinFrom, joinTo] = direction === "ancestors"
|
|
153
|
+
? [`t."${parentKey}"`, `h."${childKey}"`] // To find a parent, match table's PK to hierarchy's FK
|
|
154
|
+
: [`t."${childKey}"`, `h."${parentKey}"`]; // To find children, match table's FK to hierarchy's PK
|
|
155
|
+
const cteBodySql = `
|
|
156
|
+
(${anchorSql})
|
|
157
|
+
UNION ALL
|
|
158
|
+
SELECT t.*
|
|
159
|
+
FROM "${tableName}" AS t
|
|
160
|
+
JOIN "${cteName}" AS h ON ${joinFrom} = ${joinTo}
|
|
161
|
+
`;
|
|
162
|
+
query = kx.withRecursive(cteName, kx.raw(cteBodySql, anchorBindings)).from(fromClause);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
query = kx(fromClause);
|
|
166
|
+
}
|
|
167
|
+
// 2. Process Joins
|
|
168
|
+
if (options.joins?.length) {
|
|
169
|
+
options.joins.forEach((join) => {
|
|
170
|
+
// Handle table aliasing (e.g., 'users as u')
|
|
171
|
+
const tableToJoin = join.as ? `${join.table} as ${join.as}` : join.table;
|
|
172
|
+
switch (join.type) {
|
|
173
|
+
case "inner":
|
|
174
|
+
query.innerJoin(tableToJoin, join.onLeft, join.onRight);
|
|
175
|
+
break;
|
|
176
|
+
case "left":
|
|
177
|
+
query.leftJoin(tableToJoin, join.onLeft, join.onRight);
|
|
178
|
+
break;
|
|
179
|
+
case "right":
|
|
180
|
+
query.rightJoin(tableToJoin, join.onLeft, join.onRight);
|
|
181
|
+
break;
|
|
182
|
+
case "full_outer":
|
|
183
|
+
query.fullOuterJoin(tableToJoin, join.onLeft, join.onRight);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// 3. Process Columns (SELECT)
|
|
189
|
+
const selections = options.select.map((col) => {
|
|
190
|
+
if (typeof col === "string") {
|
|
191
|
+
return col;
|
|
192
|
+
}
|
|
193
|
+
if ("func" in col) {
|
|
194
|
+
// Use knex.raw for aggregate functions to prevent SQL injection
|
|
195
|
+
return kx.raw(`${col.func.toUpperCase()}(??) as ??`, [col.column, col.as]);
|
|
196
|
+
}
|
|
197
|
+
// Handle aliasing
|
|
198
|
+
return col.as ? `${col.column} as ${col.as}` : col.column;
|
|
199
|
+
});
|
|
200
|
+
query.select(selections);
|
|
201
|
+
// 4. Process Filters (WHERE), starting with depth 1
|
|
202
|
+
options.where.forEach((group) => applyFilters(query, group, 1));
|
|
203
|
+
// 5. Process Group By
|
|
204
|
+
if (options.groupBy?.length) {
|
|
205
|
+
query.groupBy(options.groupBy);
|
|
206
|
+
}
|
|
207
|
+
// 6. Process Sorts (ORDER BY)
|
|
208
|
+
if (options.orderBy?.length) {
|
|
209
|
+
// Knex's orderBy can take an array of objects directly
|
|
210
|
+
query.orderBy(options.orderBy.map((s) => ({ column: s.column, order: s.direction })));
|
|
211
|
+
}
|
|
212
|
+
// 7. Process Limit
|
|
213
|
+
query.limit(limit);
|
|
214
|
+
// 8. Process Offset
|
|
215
|
+
query.offset(offset);
|
|
216
|
+
return query;
|
|
217
|
+
}
|