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.
- package/.editorconfig +12 -0
- package/.gitattributes +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +34 -0
- package/.vscode/settings.json +14 -0
- package/README.md +105 -0
- package/mcr.config.ts +12 -0
- package/package.json +30 -0
- package/src/drivers/_utils.ts +31 -0
- package/src/drivers/d1.ts +70 -0
- package/src/drivers/do.ts +81 -0
- package/src/index.ts +13 -0
- package/src/queries/_utils.ts +10 -0
- package/src/queries/delete.ts +26 -0
- package/src/queries/insert.ts +22 -0
- package/src/queries/select.ts +64 -0
- package/src/queries/update.ts +28 -0
- package/src/queries/where/operators.ts +96 -0
- package/src/queries/where/where.ts +50 -0
- package/src/types.ts +76 -0
- package/test/__fixtures__/d1/worker.ts +70 -0
- package/test/__fixtures__/d1/wrangler.jsonc +13 -0
- package/test/__fixtures__/do/worker.ts +106 -0
- package/test/__fixtures__/do/wrangler.jsonc +20 -0
- package/test/e2e/d1.test.ts +113 -0
- package/test/e2e/do.test.ts +116 -0
- package/test/unit/buildDelete.test.ts +36 -0
- package/test/unit/buildInsert.test.ts +36 -0
- package/test/unit/buildSelect.test.ts +137 -0
- package/test/unit/buildUpdate.test.ts +34 -0
- package/test/unit/buildWhere.test.ts +249 -0
- package/test/unit/operators.test.ts +98 -0
- package/test/unit/prepare.test.ts +22 -0
- package/test/unit/quoteIdentifier.test.ts +12 -0
- package/test/unit/returnsRows.test.ts +41 -0
- package/test/unit/setMeta.test.ts +31 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +34 -0
- package/website/.prettierignore +4 -0
- package/website/.prettierrc +34 -0
- package/website/README.md +3 -0
- package/website/docs/documentation/connection.mdx +123 -0
- package/website/docs/documentation/queries/_category_.json +7 -0
- package/website/docs/documentation/queries/delete.mdx +51 -0
- package/website/docs/documentation/queries/insert.mdx +63 -0
- package/website/docs/documentation/queries/operators/_category_.json +7 -0
- package/website/docs/documentation/queries/operators/between.mdx +17 -0
- package/website/docs/documentation/queries/operators/eq.mdx +22 -0
- package/website/docs/documentation/queries/operators/gt.mdx +22 -0
- package/website/docs/documentation/queries/operators/gte.mdx +22 -0
- package/website/docs/documentation/queries/operators/in.mdx +28 -0
- package/website/docs/documentation/queries/operators/is-not-null.mdx +22 -0
- package/website/docs/documentation/queries/operators/is-null.mdx +22 -0
- package/website/docs/documentation/queries/operators/like.mdx +27 -0
- package/website/docs/documentation/queries/operators/lt.mdx +22 -0
- package/website/docs/documentation/queries/operators/lte.mdx +22 -0
- package/website/docs/documentation/queries/operators/ne.mdx +22 -0
- package/website/docs/documentation/queries/operators/not-between.mdx +17 -0
- package/website/docs/documentation/queries/operators/not-like.mdx +27 -0
- package/website/docs/documentation/queries/operators/notIn.mdx +28 -0
- package/website/docs/documentation/queries/select.mdx +294 -0
- package/website/docs/documentation/queries/update.mdx +61 -0
- package/website/docs/documentation/queries/where.mdx +176 -0
- package/website/docs/index.mdx +228 -0
- package/website/docusaurus.config.ts +82 -0
- package/website/package-lock.json +21348 -0
- package/website/package.json +59 -0
- package/website/plugins/locale.ts +16 -0
- package/website/sidebars.ts +18 -0
- package/website/src/components/FAQ.tsx +35 -0
- package/website/src/components/Loading.tsx +4 -0
- package/website/src/components/PageTitle.tsx +29 -0
- package/website/src/css/_faq.scss +39 -0
- package/website/src/css/_loading.scss +43 -0
- package/website/src/css/_mixins.scss +19 -0
- package/website/src/css/custom.scss +149 -0
- package/website/src/pages/index.tsx +17 -0
- package/website/static/.nojekyll +0 -0
- package/website/static/img/favicon.svg +1 -0
- package/website/test/unit/check-extensions.test.ts +48 -0
- package/website/tsconfig.json +14 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Condition,
|
|
3
|
+
Connector,
|
|
4
|
+
Param,
|
|
5
|
+
WhereClause,
|
|
6
|
+
WhereItem,
|
|
7
|
+
} from '../../types.js';
|
|
8
|
+
|
|
9
|
+
const isConnector = (value: WhereItem): value is Connector =>
|
|
10
|
+
typeof value === 'string' && ['AND', 'OR', 'XOR', 'NOT'].includes(value);
|
|
11
|
+
|
|
12
|
+
const isCondition = (value: WhereItem): value is Condition =>
|
|
13
|
+
typeof value === 'object' && !Array.isArray(value) && 'condition' in value;
|
|
14
|
+
|
|
15
|
+
const processItems = (items: WhereItem[]): { sql: string; params: Param[] } => {
|
|
16
|
+
const parts: string[] = [];
|
|
17
|
+
const params: Param[] = [];
|
|
18
|
+
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
if (isConnector(item)) {
|
|
21
|
+
parts.push(item);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isCondition(item)) {
|
|
26
|
+
parts.push(item.condition);
|
|
27
|
+
params.push(...item.params);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(item)) {
|
|
32
|
+
const group = processItems(item);
|
|
33
|
+
|
|
34
|
+
parts.push(`(${group.sql})`);
|
|
35
|
+
params.push(...group.params);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { sql: parts.join(' '), params };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const buildWhere = (
|
|
43
|
+
where: WhereClause
|
|
44
|
+
): { sql: string; params: Param[] } => {
|
|
45
|
+
if (typeof where === 'string') return { sql: where, params: [] };
|
|
46
|
+
if (isCondition(where))
|
|
47
|
+
return { sql: where.condition, params: [...where.params] };
|
|
48
|
+
|
|
49
|
+
return processItems(where);
|
|
50
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type Param = string | number | boolean | null;
|
|
2
|
+
|
|
3
|
+
export type Condition = {
|
|
4
|
+
condition: string;
|
|
5
|
+
params: Param[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type Connector = 'AND' | 'OR' | 'XOR' | 'NOT';
|
|
9
|
+
|
|
10
|
+
export type WhereItem = Condition | Connector | WhereItem[];
|
|
11
|
+
|
|
12
|
+
export type WhereClause = string | Condition | WhereItem[];
|
|
13
|
+
|
|
14
|
+
type Values = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export type InsertOptions = {
|
|
17
|
+
table: string;
|
|
18
|
+
values: Values | Values[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type JoinOptions = {
|
|
22
|
+
type: 'left' | 'right' | 'inner' | 'cross';
|
|
23
|
+
table: string;
|
|
24
|
+
on: {
|
|
25
|
+
a: string;
|
|
26
|
+
b: string;
|
|
27
|
+
};
|
|
28
|
+
outer?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SelectOptions = {
|
|
32
|
+
distinct?: boolean;
|
|
33
|
+
columns?: string | string[];
|
|
34
|
+
table: string;
|
|
35
|
+
join?: JoinOptions | JoinOptions[];
|
|
36
|
+
where?: WhereClause;
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
groupBy?: string;
|
|
40
|
+
orderBy?: [string] | [string, 'ASC' | 'DESC'];
|
|
41
|
+
params?: unknown[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type UpdateOptions = {
|
|
45
|
+
table: string;
|
|
46
|
+
set: Record<string, unknown>;
|
|
47
|
+
where?: WhereClause;
|
|
48
|
+
params?: unknown[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type DeleteOptions = {
|
|
52
|
+
table: string;
|
|
53
|
+
where?: WhereClause;
|
|
54
|
+
limit?: number;
|
|
55
|
+
params?: unknown[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type Meta = D1Meta & Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
export type QueryResult<T> = { rows: T[]; meta: Meta };
|
|
61
|
+
|
|
62
|
+
export type Connection = {
|
|
63
|
+
query: <T = Record<string, unknown>>(
|
|
64
|
+
sql: string,
|
|
65
|
+
values?: unknown[]
|
|
66
|
+
) => Promise<QueryResult<T>>;
|
|
67
|
+
select: {
|
|
68
|
+
<T = Record<string, unknown>>(
|
|
69
|
+
options: SelectOptions & { limit: 1 }
|
|
70
|
+
): Promise<T | null>;
|
|
71
|
+
<T = Record<string, unknown>>(options: SelectOptions): Promise<T[]>;
|
|
72
|
+
};
|
|
73
|
+
insert: (options: InsertOptions) => Promise<Meta>;
|
|
74
|
+
update: (options: UpdateOptions) => Promise<Meta>;
|
|
75
|
+
delete: (options: DeleteOptions) => Promise<Meta>;
|
|
76
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { OP, useD1 } from '../../../src/index.js';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
DB: D1Database;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
9
|
+
const url = new URL(request.url);
|
|
10
|
+
const db = useD1(env.DB);
|
|
11
|
+
|
|
12
|
+
switch (url.pathname) {
|
|
13
|
+
case '/setup': {
|
|
14
|
+
await db.query('DROP TABLE IF EXISTS users');
|
|
15
|
+
await db.query(
|
|
16
|
+
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL)'
|
|
17
|
+
);
|
|
18
|
+
return Response.json({ ok: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case '/query': {
|
|
22
|
+
const sql = url.searchParams.get('sql')!;
|
|
23
|
+
const result = await db.query(sql);
|
|
24
|
+
return Response.json(result);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case '/insert': {
|
|
28
|
+
const name = url.searchParams.get('name')!;
|
|
29
|
+
const email = url.searchParams.get('email')!;
|
|
30
|
+
const meta = await db.insert({
|
|
31
|
+
table: 'users',
|
|
32
|
+
values: { name, email },
|
|
33
|
+
});
|
|
34
|
+
return Response.json(meta);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case '/select': {
|
|
38
|
+
const limit = url.searchParams.get('limit');
|
|
39
|
+
const rows = await db.select({
|
|
40
|
+
table: 'users',
|
|
41
|
+
...(limit ? { limit: Number(limit) } : {}),
|
|
42
|
+
});
|
|
43
|
+
return Response.json({ rows });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case '/update': {
|
|
47
|
+
const name = url.searchParams.get('name')!;
|
|
48
|
+
const where = url.searchParams.get('where')!;
|
|
49
|
+
const meta = await db.update({
|
|
50
|
+
table: 'users',
|
|
51
|
+
set: { name },
|
|
52
|
+
where: OP.eq('name', where),
|
|
53
|
+
});
|
|
54
|
+
return Response.json(meta);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case '/delete': {
|
|
58
|
+
const name = url.searchParams.get('name')!;
|
|
59
|
+
const meta = await db.delete({
|
|
60
|
+
table: 'users',
|
|
61
|
+
where: OP.eq('name', name),
|
|
62
|
+
});
|
|
63
|
+
return Response.json(meta);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
default:
|
|
67
|
+
return new Response('Not found', { status: 404 });
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "d1-test",
|
|
4
|
+
"main": "worker.ts",
|
|
5
|
+
"compatibility_date": "2026-01-28",
|
|
6
|
+
"d1_databases": [
|
|
7
|
+
{
|
|
8
|
+
"binding": "DB",
|
|
9
|
+
"database_name": "d1-tutorial-db",
|
|
10
|
+
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Connection } from '../../../src/types.js';
|
|
2
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
3
|
+
import { OP, useDO } from '../../../src/index.js';
|
|
4
|
+
|
|
5
|
+
interface Env {
|
|
6
|
+
MY_DO: DurableObjectNamespace<MyDurableObject>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MyDurableObject extends DurableObject<Env> {
|
|
10
|
+
private db: Connection;
|
|
11
|
+
|
|
12
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
13
|
+
super(ctx, env);
|
|
14
|
+
this.db = useDO(ctx.storage.sql);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async setup() {
|
|
18
|
+
await this.db.query('DROP TABLE IF EXISTS users');
|
|
19
|
+
await this.db.query(
|
|
20
|
+
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL)'
|
|
21
|
+
);
|
|
22
|
+
return { ok: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async queryRaw(sql: string) {
|
|
26
|
+
return this.db.query(sql);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async insertUser(name: string, email: string) {
|
|
30
|
+
return this.db.insert({
|
|
31
|
+
table: 'users',
|
|
32
|
+
values: { name, email },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async selectUsers(limit?: number) {
|
|
37
|
+
return this.db.select({
|
|
38
|
+
table: 'users',
|
|
39
|
+
...(limit ? { limit: limit as 1 } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async updateUser(oldName: string, newName: string) {
|
|
44
|
+
return this.db.update({
|
|
45
|
+
table: 'users',
|
|
46
|
+
set: { name: newName },
|
|
47
|
+
where: OP.eq('name', oldName),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async deleteUser(name: string) {
|
|
52
|
+
return this.db.delete({
|
|
53
|
+
table: 'users',
|
|
54
|
+
where: OP.eq('name', name),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default {
|
|
60
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
61
|
+
const url = new URL(request.url);
|
|
62
|
+
const DO = env.MY_DO.get(env.MY_DO.idFromName('default'));
|
|
63
|
+
|
|
64
|
+
switch (url.pathname) {
|
|
65
|
+
case '/setup': {
|
|
66
|
+
const result = await DO.setup();
|
|
67
|
+
return Response.json(result);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case '/query': {
|
|
71
|
+
const sql = url.searchParams.get('sql')!;
|
|
72
|
+
const result = await DO.queryRaw(sql);
|
|
73
|
+
return Response.json(result);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case '/insert': {
|
|
77
|
+
const name = url.searchParams.get('name')!;
|
|
78
|
+
const email = url.searchParams.get('email')!;
|
|
79
|
+
const meta = await DO.insertUser(name, email);
|
|
80
|
+
return Response.json(meta);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case '/select': {
|
|
84
|
+
const limit = url.searchParams.get('limit');
|
|
85
|
+
const rows = await DO.selectUsers(limit ? Number(limit) : undefined);
|
|
86
|
+
return Response.json({ rows });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case '/update': {
|
|
90
|
+
const oldName = url.searchParams.get('oldName')!;
|
|
91
|
+
const newName = url.searchParams.get('newName')!;
|
|
92
|
+
const meta = await DO.updateUser(oldName, newName);
|
|
93
|
+
return Response.json(meta);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case '/delete': {
|
|
97
|
+
const name = url.searchParams.get('name')!;
|
|
98
|
+
const meta = await DO.deleteUser(name);
|
|
99
|
+
return Response.json(meta);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
return new Response('Not found', { status: 404 });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "do-test",
|
|
4
|
+
"main": "worker.ts",
|
|
5
|
+
"compatibility_date": "2026-01-28",
|
|
6
|
+
"durable_objects": {
|
|
7
|
+
"bindings": [
|
|
8
|
+
{
|
|
9
|
+
"name": "MY_DO",
|
|
10
|
+
"class_name": "MyDurableObject"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"migrations": [
|
|
15
|
+
{
|
|
16
|
+
"tag": "v1",
|
|
17
|
+
"new_sqlite_classes": ["MyDurableObject"]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { assert, beforeEach, describe, it, kill, startScript } from 'poku';
|
|
2
|
+
|
|
3
|
+
const PORT = 8787;
|
|
4
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
5
|
+
|
|
6
|
+
describe(async () => {
|
|
7
|
+
await kill.port(PORT);
|
|
8
|
+
|
|
9
|
+
const server = await startScript('dev:d1', {
|
|
10
|
+
startAfter: 'Ready on',
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
verbose: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await describe('useD1', async () => {
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
const res = await fetch(`${BASE_URL}/setup`);
|
|
19
|
+
assert.strictEqual(res.status, 200);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await it('should insert a row', async () => {
|
|
23
|
+
const res = await fetch(
|
|
24
|
+
`${BASE_URL}/insert?name=Alice&email=alice@example.com`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(res.status, 200);
|
|
28
|
+
|
|
29
|
+
const meta = await res.json();
|
|
30
|
+
|
|
31
|
+
assert.ok(meta);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await it('should select multiple rows', async () => {
|
|
35
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${BASE_URL}/select`);
|
|
38
|
+
|
|
39
|
+
assert.strictEqual(res.status, 200);
|
|
40
|
+
|
|
41
|
+
const data = (await res.json()) as {
|
|
42
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
assert.ok(Array.isArray(data.rows));
|
|
46
|
+
assert.ok(data.rows.length > 0);
|
|
47
|
+
assert.strictEqual(data.rows[0].name, 'Alice');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await it('should select a single row with limit 1', async () => {
|
|
51
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
52
|
+
|
|
53
|
+
const res = await fetch(`${BASE_URL}/select?limit=1`);
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(res.status, 200);
|
|
56
|
+
|
|
57
|
+
const data = (await res.json()) as {
|
|
58
|
+
rows: { id: number; name: string; email: string };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
assert.ok(data.rows !== null);
|
|
62
|
+
assert.strictEqual(data.rows.name, 'Alice');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await it('should run a raw query', async () => {
|
|
66
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
67
|
+
|
|
68
|
+
const res = await fetch(
|
|
69
|
+
`${BASE_URL}/query?sql=${encodeURIComponent('SELECT * FROM users')}`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(res.status, 200);
|
|
73
|
+
|
|
74
|
+
const data = (await res.json()) as {
|
|
75
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
assert.ok(data.rows.length > 0);
|
|
79
|
+
assert.strictEqual(data.rows[0].name, 'Alice');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await it('should update a row', async () => {
|
|
83
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
84
|
+
const res = await fetch(`${BASE_URL}/update?name=Bob&where=Alice`);
|
|
85
|
+
|
|
86
|
+
assert.strictEqual(res.status, 200);
|
|
87
|
+
|
|
88
|
+
const selectRes = await fetch(`${BASE_URL}/select`);
|
|
89
|
+
const data = (await selectRes.json()) as {
|
|
90
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(data.rows[0].name, 'Bob');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it('should delete a row', async () => {
|
|
97
|
+
await fetch(`${BASE_URL}/insert?name=Bob&email=bob@example.com`);
|
|
98
|
+
const res = await fetch(`${BASE_URL}/delete?name=Bob`);
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(res.status, 200);
|
|
101
|
+
|
|
102
|
+
const selectRes = await fetch(`${BASE_URL}/select`);
|
|
103
|
+
const data = (await selectRes.json()) as {
|
|
104
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
assert.strictEqual(data.rows.length, 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
} finally {
|
|
111
|
+
await server.end(PORT);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { assert, beforeEach, describe, it, kill, startScript } from 'poku';
|
|
2
|
+
|
|
3
|
+
const PORT = 8788;
|
|
4
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
5
|
+
|
|
6
|
+
describe(async () => {
|
|
7
|
+
await kill.port(PORT);
|
|
8
|
+
|
|
9
|
+
const server = await startScript('dev:do', {
|
|
10
|
+
startAfter: 'Ready on',
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
verbose: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await describe('useDO', async () => {
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
const res = await fetch(`${BASE_URL}/setup`);
|
|
19
|
+
|
|
20
|
+
assert.strictEqual(res.status, 200);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await it('should insert a row', async () => {
|
|
24
|
+
const res = await fetch(
|
|
25
|
+
`${BASE_URL}/insert?name=Alice&email=alice@example.com`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
assert.strictEqual(res.status, 200);
|
|
29
|
+
|
|
30
|
+
const meta = await res.json();
|
|
31
|
+
|
|
32
|
+
assert.ok(meta);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await it('should select multiple rows', async () => {
|
|
36
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
37
|
+
|
|
38
|
+
const res = await fetch(`${BASE_URL}/select`);
|
|
39
|
+
|
|
40
|
+
assert.strictEqual(res.status, 200);
|
|
41
|
+
|
|
42
|
+
const data = (await res.json()) as {
|
|
43
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
assert.ok(Array.isArray(data.rows));
|
|
47
|
+
assert.ok(data.rows.length > 0);
|
|
48
|
+
assert.strictEqual(data.rows[0].name, 'Alice');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await it('should select a single row with limit 1', async () => {
|
|
52
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
53
|
+
|
|
54
|
+
const res = await fetch(`${BASE_URL}/select?limit=1`);
|
|
55
|
+
|
|
56
|
+
assert.strictEqual(res.status, 200);
|
|
57
|
+
|
|
58
|
+
const data = (await res.json()) as {
|
|
59
|
+
rows: { id: number; name: string; email: string };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
assert.ok(data.rows !== null);
|
|
63
|
+
assert.strictEqual(data.rows.name, 'Alice');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await it('should run a raw query', async () => {
|
|
67
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
68
|
+
|
|
69
|
+
const res = await fetch(
|
|
70
|
+
`${BASE_URL}/query?sql=${encodeURIComponent('SELECT * FROM users')}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(res.status, 200);
|
|
74
|
+
|
|
75
|
+
const data = (await res.json()) as {
|
|
76
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
assert.ok(data.rows.length > 0);
|
|
80
|
+
assert.strictEqual(data.rows[0].name, 'Alice');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await it('should update a user', async () => {
|
|
84
|
+
await fetch(`${BASE_URL}/insert?name=Alice&email=alice@example.com`);
|
|
85
|
+
|
|
86
|
+
const res = await fetch(`${BASE_URL}/update?oldName=Alice&newName=Bob`);
|
|
87
|
+
|
|
88
|
+
assert.strictEqual(res.status, 200);
|
|
89
|
+
|
|
90
|
+
const selectRes = await fetch(`${BASE_URL}/select`);
|
|
91
|
+
const data = (await selectRes.json()) as {
|
|
92
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(data.rows[0].name, 'Bob');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await it('should delete a user', async () => {
|
|
99
|
+
await fetch(`${BASE_URL}/insert?name=Bob&email=bob@example.com`);
|
|
100
|
+
|
|
101
|
+
const res = await fetch(`${BASE_URL}/delete?name=Bob`);
|
|
102
|
+
|
|
103
|
+
assert.strictEqual(res.status, 200);
|
|
104
|
+
|
|
105
|
+
const selectRes = await fetch(`${BASE_URL}/select`);
|
|
106
|
+
const data = (await selectRes.json()) as {
|
|
107
|
+
rows: Array<{ id: number; name: string; email: string }>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
assert.strictEqual(data.rows.length, 0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
} finally {
|
|
114
|
+
await server.end(PORT);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assert, describe, it } from 'poku';
|
|
2
|
+
import { buildDelete } from '../../src/queries/delete.js';
|
|
3
|
+
import { OP } from '../../src/queries/where/operators.js';
|
|
4
|
+
|
|
5
|
+
describe('buildDelete', () => {
|
|
6
|
+
it('should build a basic delete', () => {
|
|
7
|
+
const result = buildDelete({ table: 'users' });
|
|
8
|
+
assert.strictEqual(result.sql, 'DELETE FROM `users`');
|
|
9
|
+
assert.deepStrictEqual(result.params, []);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should build delete with where clause', () => {
|
|
13
|
+
const result = buildDelete({
|
|
14
|
+
table: 'users',
|
|
15
|
+
where: OP.eq('name', 'Alice'),
|
|
16
|
+
});
|
|
17
|
+
assert.strictEqual(result.sql, 'DELETE FROM `users` WHERE `name` = ?');
|
|
18
|
+
assert.deepStrictEqual(result.params, ['Alice']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should build delete with limit', () => {
|
|
22
|
+
const result = buildDelete({ table: 'users', limit: 1 });
|
|
23
|
+
assert.strictEqual(result.sql, 'DELETE FROM `users` LIMIT ?');
|
|
24
|
+
assert.deepStrictEqual(result.params, [1]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should build delete with extra params', () => {
|
|
28
|
+
const result = buildDelete({
|
|
29
|
+
table: 'users',
|
|
30
|
+
where: OP.eq('id', 1),
|
|
31
|
+
params: ['extra'],
|
|
32
|
+
});
|
|
33
|
+
assert.strictEqual(result.sql, 'DELETE FROM `users` WHERE `id` = ?');
|
|
34
|
+
assert.deepStrictEqual(result.params, ['extra', 1]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assert, describe, it } from 'poku';
|
|
2
|
+
import { buildInsert } from '../../src/queries/insert.js';
|
|
3
|
+
|
|
4
|
+
describe('buildInsert', () => {
|
|
5
|
+
it('should build a single row insert', () => {
|
|
6
|
+
const result = buildInsert({
|
|
7
|
+
table: 'users',
|
|
8
|
+
values: { name: 'Alice', email: 'a@b.com' },
|
|
9
|
+
});
|
|
10
|
+
assert.strictEqual(
|
|
11
|
+
result.sql,
|
|
12
|
+
'INSERT INTO `users` (`name`, `email`) VALUES (?, ?)'
|
|
13
|
+
);
|
|
14
|
+
assert.deepStrictEqual(result.params, ['Alice', 'a@b.com']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should build a multi-row insert', () => {
|
|
18
|
+
const result = buildInsert({
|
|
19
|
+
table: 'users',
|
|
20
|
+
values: [
|
|
21
|
+
{ name: 'Alice', email: 'a@b.com' },
|
|
22
|
+
{ name: 'Bob', email: 'b@b.com' },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
assert.strictEqual(
|
|
26
|
+
result.sql,
|
|
27
|
+
'INSERT INTO `users` (`name`, `email`) VALUES (?, ?), (?, ?)'
|
|
28
|
+
);
|
|
29
|
+
assert.deepStrictEqual(result.params, [
|
|
30
|
+
'Alice',
|
|
31
|
+
'a@b.com',
|
|
32
|
+
'Bob',
|
|
33
|
+
'b@b.com',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
});
|