cl-orm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.editorconfig +12 -0
  2. package/.gitattributes +1 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc +34 -0
  5. package/.vscode/settings.json +14 -0
  6. package/README.md +105 -0
  7. package/mcr.config.ts +12 -0
  8. package/package.json +30 -0
  9. package/src/drivers/_utils.ts +31 -0
  10. package/src/drivers/d1.ts +70 -0
  11. package/src/drivers/do.ts +81 -0
  12. package/src/index.ts +13 -0
  13. package/src/queries/_utils.ts +10 -0
  14. package/src/queries/delete.ts +26 -0
  15. package/src/queries/insert.ts +22 -0
  16. package/src/queries/select.ts +64 -0
  17. package/src/queries/update.ts +28 -0
  18. package/src/queries/where/operators.ts +96 -0
  19. package/src/queries/where/where.ts +50 -0
  20. package/src/types.ts +76 -0
  21. package/test/__fixtures__/d1/worker.ts +70 -0
  22. package/test/__fixtures__/d1/wrangler.jsonc +13 -0
  23. package/test/__fixtures__/do/worker.ts +106 -0
  24. package/test/__fixtures__/do/wrangler.jsonc +20 -0
  25. package/test/e2e/d1.test.ts +113 -0
  26. package/test/e2e/do.test.ts +116 -0
  27. package/test/unit/buildDelete.test.ts +36 -0
  28. package/test/unit/buildInsert.test.ts +36 -0
  29. package/test/unit/buildSelect.test.ts +137 -0
  30. package/test/unit/buildUpdate.test.ts +34 -0
  31. package/test/unit/buildWhere.test.ts +249 -0
  32. package/test/unit/operators.test.ts +98 -0
  33. package/test/unit/prepare.test.ts +22 -0
  34. package/test/unit/quoteIdentifier.test.ts +12 -0
  35. package/test/unit/returnsRows.test.ts +41 -0
  36. package/test/unit/setMeta.test.ts +31 -0
  37. package/tsconfig.build.json +4 -0
  38. package/tsconfig.json +34 -0
  39. package/website/.prettierignore +4 -0
  40. package/website/.prettierrc +34 -0
  41. package/website/README.md +3 -0
  42. package/website/docs/documentation/connection.mdx +123 -0
  43. package/website/docs/documentation/queries/_category_.json +7 -0
  44. package/website/docs/documentation/queries/delete.mdx +51 -0
  45. package/website/docs/documentation/queries/insert.mdx +63 -0
  46. package/website/docs/documentation/queries/operators/_category_.json +7 -0
  47. package/website/docs/documentation/queries/operators/between.mdx +17 -0
  48. package/website/docs/documentation/queries/operators/eq.mdx +22 -0
  49. package/website/docs/documentation/queries/operators/gt.mdx +22 -0
  50. package/website/docs/documentation/queries/operators/gte.mdx +22 -0
  51. package/website/docs/documentation/queries/operators/in.mdx +28 -0
  52. package/website/docs/documentation/queries/operators/is-not-null.mdx +22 -0
  53. package/website/docs/documentation/queries/operators/is-null.mdx +22 -0
  54. package/website/docs/documentation/queries/operators/like.mdx +27 -0
  55. package/website/docs/documentation/queries/operators/lt.mdx +22 -0
  56. package/website/docs/documentation/queries/operators/lte.mdx +22 -0
  57. package/website/docs/documentation/queries/operators/ne.mdx +22 -0
  58. package/website/docs/documentation/queries/operators/not-between.mdx +17 -0
  59. package/website/docs/documentation/queries/operators/not-like.mdx +27 -0
  60. package/website/docs/documentation/queries/operators/notIn.mdx +28 -0
  61. package/website/docs/documentation/queries/select.mdx +294 -0
  62. package/website/docs/documentation/queries/update.mdx +61 -0
  63. package/website/docs/documentation/queries/where.mdx +176 -0
  64. package/website/docs/index.mdx +228 -0
  65. package/website/docusaurus.config.ts +82 -0
  66. package/website/package-lock.json +21348 -0
  67. package/website/package.json +59 -0
  68. package/website/plugins/locale.ts +16 -0
  69. package/website/sidebars.ts +18 -0
  70. package/website/src/components/FAQ.tsx +35 -0
  71. package/website/src/components/Loading.tsx +4 -0
  72. package/website/src/components/PageTitle.tsx +29 -0
  73. package/website/src/css/_faq.scss +39 -0
  74. package/website/src/css/_loading.scss +43 -0
  75. package/website/src/css/_mixins.scss +19 -0
  76. package/website/src/css/custom.scss +149 -0
  77. package/website/src/pages/index.tsx +17 -0
  78. package/website/static/.nojekyll +0 -0
  79. package/website/static/img/favicon.svg +1 -0
  80. package/website/test/unit/check-extensions.test.ts +48 -0
  81. package/website/tsconfig.json +14 -0
package/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
package/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ website/** linguist-documentation
@@ -0,0 +1,3 @@
1
+ /lib
2
+ /CHANGELOG.md
3
+ /website
package/.prettierrc ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "printWidth": 80,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": true,
7
+ "quoteProps": "as-needed",
8
+ "jsxSingleQuote": true,
9
+ "trailingComma": "es5",
10
+ "bracketSpacing": true,
11
+ "bracketSameLine": false,
12
+ "arrowParens": "always",
13
+ "proseWrap": "preserve",
14
+ "endOfLine": "auto",
15
+ "embeddedLanguageFormatting": "auto",
16
+ "singleAttributePerLine": false,
17
+ "plugins": ["@ianvs/prettier-plugin-sort-imports"],
18
+ "importOrder": [
19
+ "<TYPES>^(node:)",
20
+ "<TYPES>",
21
+ "<TYPES>^[.]",
22
+ "<BUILTIN_MODULES>",
23
+ "<THIRD_PARTY_MODULES>",
24
+ "^[.]"
25
+ ],
26
+ "overrides": [
27
+ {
28
+ "files": "*.jsonc",
29
+ "options": {
30
+ "trailingComma": "none"
31
+ }
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "editor.trimAutoWhitespace": true,
3
+ "editor.indentSize": 2,
4
+ "editor.tabSize": 2,
5
+ "editor.formatOnSave": true,
6
+ "files.trimTrailingWhitespace": true,
7
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
8
+ "editor.codeActionsOnSave": {
9
+ "source.fixAll": "explicit"
10
+ },
11
+ "[markdown]": {
12
+ "files.trimTrailingWhitespace": false
13
+ }
14
+ }
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # CL ORM
2
+
3
+ <img align="right" width="64" height="64" alt="Logo" src="website/static/img/favicon.svg">
4
+
5
+ A lightweight **ORM** for **Cloudflare Workers** (**D1** and **Durable Objects**), designed to be intuitive, productive and focused on essential functionality.
6
+
7
+ - This project supports **Cloudflare D1** and **Durable Objects SQL Storage** as database drivers.
8
+
9
+ ---
10
+
11
+ 📘 [**Documentation**](https://wellwelwel.github.io/cl-orm/docs/)
12
+
13
+ ---
14
+
15
+ ## Why
16
+
17
+ - Supports both **Cloudflare D1** and **Durable Objects** SQL Storage.
18
+ - Unified **Connection** interface across different database drivers.
19
+ - An user-friendly ORM for **INSERT**, **SELECT**, **UPDATE**, **DELETE** and **WHERE** clauses.
20
+ - Automatic **Prepared Statements** (including **LIMIT** and **OFFSET**).
21
+
22
+ ---
23
+
24
+ ## Documentation
25
+
26
+ See detailed specifications and usage in [**Documentation**](https://wellwelwel.github.io/cl-orm/docs/category/documentation) section for queries, advanced concepts and much more.
27
+
28
+ ---
29
+
30
+ ## Quickstart
31
+
32
+ ### Installation
33
+
34
+ ```shell
35
+ npm i cl-orm
36
+ ```
37
+
38
+ ---
39
+
40
+ ### Connect
41
+
42
+ #### D1
43
+
44
+ ```ts
45
+ import { useD1 } from 'cl-orm';
46
+
47
+ export default {
48
+ async fetch(request: Request, env: Env): Promise<Response> {
49
+ const db = useD1(env.DB);
50
+
51
+ await db.query('SELECT 1');
52
+ // ...
53
+ },
54
+ };
55
+ ```
56
+
57
+ #### Durable Objects
58
+
59
+ ```ts
60
+ import { useDO } from 'cl-orm';
61
+ import { DurableObject } from 'cloudflare:workers';
62
+
63
+ export class MyDurableObject extends DurableObject {
64
+ async fetch(request: Request): Promise<Response> {
65
+ const db = useDO(this.ctx.storage.sql);
66
+
67
+ await db.query('SELECT 1');
68
+ // ...
69
+ }
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ### Basic Usage Example
76
+
77
+ The following example is based on **TypeScript** and **ES Modules**.
78
+
79
+ ```ts
80
+ import type { Connection } from 'cl-orm';
81
+ import { OP } from 'cl-orm';
82
+
83
+ export const getUser = async (db: Connection, id: number) => {
84
+ const user = await db.select({
85
+ table: 'users',
86
+ where: OP.eq('id', id),
87
+ limit: 1,
88
+ });
89
+
90
+ return user;
91
+ };
92
+
93
+ // Usage: await getUser(db, 15);
94
+ ```
95
+
96
+ - See all available operators (**OP**) [here](https://wellwelwel.github.io/cl-orm/docs/category/operators).
97
+ - Due to `limit: 1`, it returns a direct object with the row result or `null`.
98
+
99
+ ---
100
+
101
+ ## Acknowledgements
102
+
103
+ - The operator names **eq**, **ne**, **gt**, **lt**, **gte** and **lte** are inspired by [**Sequelize**](https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#operators).
104
+ - [**Contributors**](https://github.com/wellwelwel/cl-orm/graphs/contributors).
105
+ - This project is adapted from [**MySQL2 ORM**](https://github.com/wellwelwel/mysql2-orm).
package/mcr.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { CoverageReportOptions } from 'monocart-coverage-reports';
2
+
3
+ const coverageOptions: CoverageReportOptions = {
4
+ reports: ['v8', 'console-details', 'codecov'],
5
+ entryFilter: {
6
+ '**/node_modules/**': false,
7
+ '**/test/**': false,
8
+ '**/src/**': true,
9
+ },
10
+ };
11
+
12
+ export default coverageOptions;
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "cl-orm",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight ORM for Cloudflare Workers (D1 and Durable Objects), designed to be intuitive, productive and focused on essential functionality",
5
+ "scripts": {
6
+ "dev:d1": "wrangler dev --config test/__fixtures__/d1/wrangler.jsonc --port 8787 --inspector-port 9229",
7
+ "dev:do": "wrangler dev --config test/__fixtures__/do/wrangler.jsonc --port 8788 --inspector-port 9230",
8
+ "prebuild": "rm -rf lib",
9
+ "build": "tsc -p tsconfig.build.json",
10
+ "lint": "prettier --check .",
11
+ "lint:fix": "prettier --write .",
12
+ "test": "poku test",
13
+ "test:coverage": "mcr --import tsx --config mcr.config.ts npm run test",
14
+ "typecheck": "tsc --noEmit",
15
+ "update": "pu && npm i && npm update && (npm audit fix || true)",
16
+ "postupdate": "npm run lint:fix"
17
+ },
18
+ "devDependencies": {
19
+ "@cloudflare/workers-types": "^4.20260130.0",
20
+ "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
21
+ "@types/node": "^25.1.0",
22
+ "monocart-coverage-reports": "^2.12.9",
23
+ "packages-update": "^2.0.0",
24
+ "poku": "^3.0.3-canary.13a996a9",
25
+ "prettier": "^3.8.1",
26
+ "tsx": "^4.21.0",
27
+ "typescript": "^5.9.3",
28
+ "wrangler": "^4.61.1"
29
+ }
30
+ }
@@ -0,0 +1,31 @@
1
+ import type { Meta } from '../types.js';
2
+
3
+ export const returnsRows = (sql: string): boolean => {
4
+ const trimmed = sql.trimStart().toUpperCase();
5
+
6
+ return (
7
+ trimmed.indexOf('SELECT') === 0 ||
8
+ trimmed.indexOf('WITH') === 0 ||
9
+ trimmed.indexOf('PRAGMA') === 0 ||
10
+ trimmed.indexOf('RETURNING') !== -1
11
+ );
12
+ };
13
+
14
+ export const prepare = (
15
+ db: D1Database,
16
+ sql: string,
17
+ params: unknown[]
18
+ ): D1PreparedStatement =>
19
+ params.length > 0 ? db.prepare(sql).bind(...params) : db.prepare(sql);
20
+
21
+ export const setMeta = (
22
+ cursor: SqlStorageCursor<Record<string, SqlStorageValue>>
23
+ ): Meta => ({
24
+ duration: 0,
25
+ size_after: 0,
26
+ rows_read: cursor.rowsRead,
27
+ rows_written: cursor.rowsWritten,
28
+ last_row_id: 0,
29
+ changed_db: cursor.rowsWritten > 0,
30
+ changes: cursor.rowsWritten,
31
+ });
@@ -0,0 +1,70 @@
1
+ import type {
2
+ Connection,
3
+ DeleteOptions,
4
+ InsertOptions,
5
+ Meta,
6
+ QueryResult,
7
+ SelectOptions,
8
+ UpdateOptions,
9
+ } from '../types.js';
10
+ import { buildDelete } from '../queries/delete.js';
11
+ import { buildInsert } from '../queries/insert.js';
12
+ import { buildSelect } from '../queries/select.js';
13
+ import { buildUpdate } from '../queries/update.js';
14
+ import { prepare, returnsRows } from './_utils.js';
15
+
16
+ export const useD1 = (db: D1Database): Connection => {
17
+ const query = async <T = Record<string, unknown>>(
18
+ sql: string,
19
+ values?: unknown[]
20
+ ): Promise<QueryResult<T>> => {
21
+ const statement = values
22
+ ? db.prepare(sql).bind(...values)
23
+ : db.prepare(sql);
24
+
25
+ if (returnsRows(sql)) {
26
+ const { results, meta } = await statement.all<T>();
27
+ return { rows: results, meta: meta as Meta };
28
+ }
29
+
30
+ const { meta } = await statement.run();
31
+ return { rows: [], meta: meta as Meta };
32
+ };
33
+
34
+ const select = async <T = Record<string, unknown>>(
35
+ options: SelectOptions
36
+ ): Promise<T | null | T[]> => {
37
+ const { sql, params } = buildSelect(options);
38
+ const statement = prepare(db, sql, params);
39
+
40
+ if (options.limit === 1) return statement.first<T>();
41
+
42
+ const { results } = await statement.all<T>();
43
+ return results;
44
+ };
45
+
46
+ const insert = async (options: InsertOptions): Promise<Meta> => {
47
+ const { sql, params } = buildInsert(options);
48
+ const { meta } = await query(sql, params);
49
+
50
+ return meta;
51
+ };
52
+
53
+ const update = async (options: UpdateOptions): Promise<Meta> => {
54
+ const { sql, params } = buildUpdate(options);
55
+ const statement = prepare(db, sql, params);
56
+ const { meta } = await statement.run();
57
+
58
+ return meta as Meta;
59
+ };
60
+
61
+ const del = async (options: DeleteOptions): Promise<Meta> => {
62
+ const { sql, params } = buildDelete(options);
63
+ const statement = prepare(db, sql, params);
64
+ const { meta } = await statement.run();
65
+
66
+ return meta as Meta;
67
+ };
68
+
69
+ return { query, select, insert, update, delete: del };
70
+ };
@@ -0,0 +1,81 @@
1
+ import type {
2
+ Connection,
3
+ DeleteOptions,
4
+ InsertOptions,
5
+ Meta,
6
+ QueryResult,
7
+ SelectOptions,
8
+ UpdateOptions,
9
+ } from '../types.js';
10
+ import { buildDelete } from '../queries/delete.js';
11
+ import { buildInsert } from '../queries/insert.js';
12
+ import { buildSelect } from '../queries/select.js';
13
+ import { buildUpdate } from '../queries/update.js';
14
+ import { returnsRows, setMeta } from './_utils.js';
15
+
16
+ export const useDO = (sql: SqlStorage): Connection => {
17
+ const exec = <T extends Record<string, SqlStorageValue>>(
18
+ query: string,
19
+ values?: unknown[]
20
+ ) => (values ? sql.exec<T>(query, ...values) : sql.exec<T>(query));
21
+
22
+ const query = async <T = Record<string, unknown>>(
23
+ queryStr: string,
24
+ values?: unknown[]
25
+ ): Promise<QueryResult<T>> => {
26
+ const cursor = exec(queryStr, values);
27
+
28
+ if (returnsRows(queryStr)) {
29
+ const rows = cursor.toArray() as T[];
30
+ return { rows, meta: setMeta(cursor) };
31
+ }
32
+
33
+ return { rows: [], meta: setMeta(cursor) };
34
+ };
35
+
36
+ const select = async <T = Record<string, unknown>>(
37
+ options: SelectOptions
38
+ ): Promise<T | null | T[]> => {
39
+ const built = buildSelect(options);
40
+ const cursor = exec(
41
+ built.sql,
42
+ built.params.length > 0 ? built.params : undefined
43
+ );
44
+
45
+ if (options.limit === 1) {
46
+ const rows = cursor.toArray() as T[];
47
+ return rows[0] ?? null;
48
+ }
49
+
50
+ return cursor.toArray() as T[];
51
+ };
52
+
53
+ const insert = async (options: InsertOptions): Promise<Meta> => {
54
+ const built = buildInsert(options);
55
+ const cursor = exec(built.sql, built.params);
56
+
57
+ return setMeta(cursor);
58
+ };
59
+
60
+ const update = async (options: UpdateOptions): Promise<Meta> => {
61
+ const built = buildUpdate(options);
62
+ const cursor = exec(
63
+ built.sql,
64
+ built.params.length > 0 ? built.params : undefined
65
+ );
66
+
67
+ return setMeta(cursor);
68
+ };
69
+
70
+ const del = async (options: DeleteOptions): Promise<Meta> => {
71
+ const built = buildDelete(options);
72
+ const cursor = exec(
73
+ built.sql,
74
+ built.params.length > 0 ? built.params : undefined
75
+ );
76
+
77
+ return setMeta(cursor);
78
+ };
79
+
80
+ return { query, select, insert, update, delete: del };
81
+ };
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { useD1 } from './drivers/d1.js';
2
+ export { useDO } from './drivers/do.js';
3
+ export { OP } from './queries/where/operators.js';
4
+
5
+ export type {
6
+ QueryResult,
7
+ Connection,
8
+ Meta,
9
+ Condition,
10
+ Param,
11
+ WhereClause,
12
+ WhereItem,
13
+ } from './types.js';
@@ -0,0 +1,10 @@
1
+ export const quoteIdentifier = (name: string): string => {
2
+ if (name.includes('.')) {
3
+ return name
4
+ .split('.')
5
+ .map((part) => `\`${part}\``)
6
+ .join('.');
7
+ }
8
+
9
+ return `\`${name}\``;
10
+ };
@@ -0,0 +1,26 @@
1
+ import type { DeleteOptions } from '../types.js';
2
+ import { quoteIdentifier } from './_utils.js';
3
+ import { buildWhere } from './where/where.js';
4
+
5
+ export const buildDelete = (
6
+ options: DeleteOptions
7
+ ): { sql: string; params: unknown[] } => {
8
+ const parts: string[] = ['DELETE FROM', quoteIdentifier(options.table)];
9
+ const params: unknown[] = [];
10
+
11
+ if (options.where) {
12
+ const where = buildWhere(options.where);
13
+
14
+ parts.push('WHERE', where.sql);
15
+ params.push(...where.params);
16
+ }
17
+
18
+ if (options.limit !== undefined) {
19
+ parts.push('LIMIT ?');
20
+ params.push(options.limit);
21
+ }
22
+
23
+ if (options.params) params.unshift(...options.params);
24
+
25
+ return { sql: parts.join(' '), params };
26
+ };
@@ -0,0 +1,22 @@
1
+ import type { InsertOptions } from '../types.js';
2
+ import { quoteIdentifier } from './_utils.js';
3
+
4
+ export const buildInsert = (
5
+ options: InsertOptions
6
+ ): { sql: string; params: unknown[] } => {
7
+ const rows = Array.isArray(options.values)
8
+ ? options.values
9
+ : [options.values];
10
+ const columns = Object.keys(rows[0]);
11
+
12
+ const columnsSql = columns.map(quoteIdentifier).join(', ');
13
+ const placeholders = `(${columns.map(() => '?').join(', ')})`;
14
+ const valuesSql = rows.map(() => placeholders).join(', ');
15
+
16
+ const params = rows.flatMap((row) => columns.map((col) => row[col]));
17
+
18
+ return {
19
+ sql: `INSERT INTO ${quoteIdentifier(options.table)} (${columnsSql}) VALUES ${valuesSql}`,
20
+ params,
21
+ };
22
+ };
@@ -0,0 +1,64 @@
1
+ import type { JoinOptions, SelectOptions } from '../types.js';
2
+ import { quoteIdentifier } from './_utils.js';
3
+ import { buildWhere } from './where/where.js';
4
+
5
+ const buildColumns = (columns?: string | string[]): string => {
6
+ if (!columns) return '*';
7
+ if (typeof columns === 'string') return columns;
8
+
9
+ return columns.map(quoteIdentifier).join(', ');
10
+ };
11
+
12
+ const buildJoin = (join: JoinOptions): string => {
13
+ const type = join.outer
14
+ ? `${join.type.toUpperCase()} OUTER`
15
+ : join.type.toUpperCase();
16
+
17
+ return `${type} JOIN ${quoteIdentifier(join.table)} ON ${quoteIdentifier(join.on.a)} = ${quoteIdentifier(join.on.b)}`;
18
+ };
19
+
20
+ export const buildSelect = (
21
+ options: SelectOptions
22
+ ): { sql: string; params: unknown[] } => {
23
+ const parts: string[] = ['SELECT'];
24
+ const params: unknown[] = [];
25
+
26
+ if (options.distinct) parts.push('DISTINCT');
27
+
28
+ parts.push(buildColumns(options.columns));
29
+ parts.push('FROM', quoteIdentifier(options.table));
30
+
31
+ if (options.join) {
32
+ const joins = Array.isArray(options.join) ? options.join : [options.join];
33
+ for (const join of joins) {
34
+ parts.push(buildJoin(join));
35
+ }
36
+ }
37
+
38
+ if (options.where) {
39
+ const where = buildWhere(options.where);
40
+ parts.push('WHERE', where.sql);
41
+ params.push(...where.params);
42
+ }
43
+
44
+ if (options.groupBy) parts.push('GROUP BY', quoteIdentifier(options.groupBy));
45
+
46
+ if (options.orderBy) {
47
+ const [column, direction = 'ASC'] = options.orderBy;
48
+ parts.push('ORDER BY', quoteIdentifier(column), direction);
49
+ }
50
+
51
+ if (options.limit !== undefined) {
52
+ parts.push('LIMIT ?');
53
+ params.push(options.limit);
54
+ }
55
+
56
+ if (options.offset !== undefined) {
57
+ parts.push('OFFSET ?');
58
+ params.push(options.offset);
59
+ }
60
+
61
+ if (options.params) params.unshift(...options.params);
62
+
63
+ return { sql: parts.join(' '), params };
64
+ };
@@ -0,0 +1,28 @@
1
+ import type { UpdateOptions } from '../types.js';
2
+ import { quoteIdentifier } from './_utils.js';
3
+ import { buildWhere } from './where/where.js';
4
+
5
+ export const buildUpdate = (
6
+ options: UpdateOptions
7
+ ): { sql: string; params: unknown[] } => {
8
+ const columns = Object.keys(options.set);
9
+ const setSql = columns.map((col) => `${quoteIdentifier(col)} = ?`).join(', ');
10
+ const params: unknown[] = columns.map((col) => options.set[col]);
11
+
12
+ const parts: string[] = [
13
+ 'UPDATE',
14
+ quoteIdentifier(options.table),
15
+ 'SET',
16
+ setSql,
17
+ ];
18
+
19
+ if (options.where) {
20
+ const where = buildWhere(options.where);
21
+ parts.push('WHERE', where.sql);
22
+ params.push(...where.params);
23
+ }
24
+
25
+ if (options.params) params.push(...options.params);
26
+
27
+ return { sql: parts.join(' '), params };
28
+ };
@@ -0,0 +1,96 @@
1
+ import type { Condition, Param } from '../../types.js';
2
+ import { quoteIdentifier } from '../_utils.js';
3
+
4
+ const comparison = (
5
+ column: string,
6
+ operator: string,
7
+ param: Param
8
+ ): Condition => ({
9
+ condition: `${quoteIdentifier(column)} ${operator} ?`,
10
+ params: [param],
11
+ });
12
+
13
+ export const OP = {
14
+ eq: (column: string, param: Param): Condition =>
15
+ comparison(column, '=', param),
16
+ ne: (column: string, param: Param): Condition =>
17
+ comparison(column, '!=', param),
18
+ gt: (column: string, param: Param): Condition =>
19
+ comparison(column, '>', param),
20
+ lt: (column: string, param: Param): Condition =>
21
+ comparison(column, '<', param),
22
+ gte: (column: string, param: Param): Condition =>
23
+ comparison(column, '>=', param),
24
+ lte: (column: string, param: Param): Condition =>
25
+ comparison(column, '<=', param),
26
+ like: (column: string, param: Param): Condition =>
27
+ comparison(column, 'LIKE', param),
28
+ notLike: (column: string, param: Param): Condition =>
29
+ comparison(column, 'NOT LIKE', param),
30
+
31
+ isNull: (column: string): Condition => ({
32
+ condition: `${quoteIdentifier(column)} IS NULL`,
33
+ params: [],
34
+ }),
35
+
36
+ isNotNull: (column: string): Condition => ({
37
+ condition: `${quoteIdentifier(column)} IS NOT NULL`,
38
+ params: [],
39
+ }),
40
+
41
+ in: ((
42
+ column: string,
43
+ valuesOrSubquery: Param[] | string,
44
+ subqueryParams?: Param[]
45
+ ): Condition => {
46
+ if (typeof valuesOrSubquery === 'string') {
47
+ return {
48
+ condition: `${quoteIdentifier(column)} IN (${valuesOrSubquery})`,
49
+ params: subqueryParams ?? [],
50
+ };
51
+ }
52
+
53
+ const placeholders = valuesOrSubquery.map(() => '?').join(', ');
54
+
55
+ return {
56
+ condition: `${quoteIdentifier(column)} IN (${placeholders})`,
57
+ params: valuesOrSubquery,
58
+ };
59
+ }) as {
60
+ (column: string, params: Param[]): Condition;
61
+ (column: string, subquery: string, params: Param[]): Condition;
62
+ },
63
+
64
+ notIn: ((
65
+ column: string,
66
+ valuesOrSubquery: Param[] | string,
67
+ subqueryParams?: Param[]
68
+ ): Condition => {
69
+ if (typeof valuesOrSubquery === 'string') {
70
+ return {
71
+ condition: `${quoteIdentifier(column)} NOT IN (${valuesOrSubquery})`,
72
+ params: subqueryParams ?? [],
73
+ };
74
+ }
75
+
76
+ const placeholders = valuesOrSubquery.map(() => '?').join(', ');
77
+
78
+ return {
79
+ condition: `${quoteIdentifier(column)} NOT IN (${placeholders})`,
80
+ params: valuesOrSubquery,
81
+ };
82
+ }) as {
83
+ (column: string, params: Param[]): Condition;
84
+ (column: string, subquery: string, params: Param[]): Condition;
85
+ },
86
+
87
+ between: (column: string, params: [Param, Param]): Condition => ({
88
+ condition: `${quoteIdentifier(column)} BETWEEN ? AND ?`,
89
+ params,
90
+ }),
91
+
92
+ notBetween: (column: string, params: [Param, Param]): Condition => ({
93
+ condition: `${quoteIdentifier(column)} NOT BETWEEN ? AND ?`,
94
+ params,
95
+ }),
96
+ };