@zinn-dev/core 0.0.4 → 0.0.5
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/index.ts +61 -22
- package/package.json +4 -1
- package/src/constant.ts +1 -0
- package/src/db/project.ts +46 -0
- package/src/db/task.ts +29 -0
- package/src/db.ts +33 -19
- package/src/lib.ts +4 -0
- package/src/types.ts +28 -0
package/index.ts
CHANGED
|
@@ -1,31 +1,70 @@
|
|
|
1
|
+
import type { Task } from "./src/types";
|
|
2
|
+
|
|
3
|
+
import { randomUUIDv7 } from "bun";
|
|
4
|
+
import { generateKeyBetween } from "fractional-indexing";
|
|
5
|
+
|
|
1
6
|
// TODO: do not initialize before the first "valid" command
|
|
2
7
|
import "./src/config";
|
|
3
|
-
import
|
|
8
|
+
import * as dbProject from "./src/db/project";
|
|
9
|
+
import * as dbTask from "./src/db/task";
|
|
10
|
+
import { standardizeProjectKey } from "./src/lib";
|
|
11
|
+
|
|
12
|
+
export const project = {
|
|
13
|
+
getByKey: dbProject.getByKey,
|
|
14
|
+
create: ({ key, name }: { key: string; name: string }) => {
|
|
15
|
+
const standardizedKey = standardizeProjectKey(key);
|
|
16
|
+
const project = dbProject.getByKey(standardizedKey);
|
|
17
|
+
|
|
18
|
+
if (project != null) {
|
|
19
|
+
throw new Error(`Project with key "${standardizedKey}" already exists!`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dbProject.create(standardizedKey, name);
|
|
23
|
+
},
|
|
24
|
+
delete: (key: string) => {
|
|
25
|
+
const standardizedKey = standardizeProjectKey(key);
|
|
26
|
+
const project = dbProject.getByKey(standardizedKey);
|
|
27
|
+
|
|
28
|
+
if (project == null) {
|
|
29
|
+
throw new Error(`Project with key "${standardizedKey}" does not exist!`);
|
|
30
|
+
}
|
|
4
31
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
32
|
+
dbProject.deleteByKey(standardizedKey);
|
|
33
|
+
},
|
|
34
|
+
standardizeKey: standardizeProjectKey,
|
|
35
|
+
};
|
|
9
36
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
37
|
+
export const task = {
|
|
38
|
+
create: (task: Pick<Task, "project_id" | "name" | "description">) => {
|
|
39
|
+
// TODO: if anything after this fails, especially the task creation, the counter is still incremented but not assigned to any task
|
|
40
|
+
const project = dbProject.incrementTaskCounterById(task.project_id);
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
42
|
+
if (project == null) {
|
|
43
|
+
throw new Error(`Project's next task number could not be retrieved (ID:${task.project_id})`);
|
|
44
|
+
}
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
// TODO: do proper refined query
|
|
47
|
+
// FIXME: returns all tasks from *all* projects
|
|
48
|
+
const allTasks = dbTask.getAll();
|
|
21
49
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
let previousTaskOrder = null;
|
|
51
|
+
if (allTasks.length > 0) {
|
|
52
|
+
const lastTask = allTasks[allTasks.length - 1]!;
|
|
53
|
+
previousTaskOrder = lastTask.task_order;
|
|
54
|
+
}
|
|
25
55
|
|
|
26
|
-
|
|
27
|
-
throw new Error(`Project with key "${standardizedKey}" does not exist!`);
|
|
28
|
-
}
|
|
56
|
+
const order = generateKeyBetween(previousTaskOrder, null);
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
return dbTask.create({
|
|
60
|
+
id: randomUUIDv7(),
|
|
61
|
+
project_id: task.project_id,
|
|
62
|
+
number: project.task_count,
|
|
63
|
+
name: task.name,
|
|
64
|
+
description: task.description,
|
|
65
|
+
task_order: order,
|
|
66
|
+
created_at: now,
|
|
67
|
+
updated_at: now,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zinn-dev/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Shared logic for zinn, a kanban workflow in the terminal.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,5 +22,8 @@
|
|
|
22
22
|
],
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"fractional-indexing": "4.0.0"
|
|
25
28
|
}
|
|
26
29
|
}
|
package/src/constant.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Bind, Project } from "../types";
|
|
2
|
+
|
|
3
|
+
import { randomUUIDv7 } from "bun";
|
|
4
|
+
|
|
5
|
+
import { DB_TABLE } from "../constant";
|
|
6
|
+
import { getDb } from "../db";
|
|
7
|
+
import { standardizeProjectKey } from "../lib";
|
|
8
|
+
|
|
9
|
+
export function getByKey(key: string) {
|
|
10
|
+
const db = getDb();
|
|
11
|
+
const standardizedKey = standardizeProjectKey(key);
|
|
12
|
+
|
|
13
|
+
return db
|
|
14
|
+
.query<Project, Bind<{ key: string }>>(`SELECT * FROM ${DB_TABLE.project} WHERE key = $key`)
|
|
15
|
+
.get({ $key: standardizedKey });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// TODO: switch to object param
|
|
19
|
+
export function create(key: string, name: string) {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
|
|
22
|
+
const query = db.query(`INSERT INTO
|
|
23
|
+
${DB_TABLE.project} (id, key, name, created_at, updated_at)
|
|
24
|
+
VALUES ($id, $key, $name, $created, $updated);`);
|
|
25
|
+
const time = Date.now();
|
|
26
|
+
|
|
27
|
+
return query.run({ $id: randomUUIDv7(), $key: key, $name: name, $created: time, $updated: time });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function incrementTaskCounterById(projectId: string) {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
|
|
33
|
+
const nextTaskNumber = db
|
|
34
|
+
.query<Pick<Project, "task_count">, Bind<Pick<Project, "id">>>(`UPDATE ${DB_TABLE.project}
|
|
35
|
+
SET task_count = task_count + 1
|
|
36
|
+
WHERE id = $id RETURNING task_count`)
|
|
37
|
+
.get({ $id: projectId });
|
|
38
|
+
return nextTaskNumber;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// TODO: maybe allow both by `key` and `id`, and use whichever is provided
|
|
42
|
+
export function deleteByKey(key: string) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
|
|
45
|
+
return db.query(`DELETE FROM ${DB_TABLE.project} WHERE key = $key;`).run({ $key: key });
|
|
46
|
+
}
|
package/src/db/task.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Bind, Task } from "../types";
|
|
2
|
+
|
|
3
|
+
import { getDb } from "../db";
|
|
4
|
+
import { DB_TABLE } from "../constant";
|
|
5
|
+
|
|
6
|
+
// TODO: refined parameters, preferably of type SQLBindings
|
|
7
|
+
export function getAll() {
|
|
8
|
+
const db = getDb();
|
|
9
|
+
return db.query<Task, any>(`SELECT * FROM ${DB_TABLE.task}`).all();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function create(task: Omit<Task, "archived_at">) {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
|
|
15
|
+
const q = db.query<Task, Bind<Omit<Task, "archived_at">>>(`INSERT INTO
|
|
16
|
+
${DB_TABLE.task} (id, project_id, number, name, description, task_order, created_at, updated_at)
|
|
17
|
+
VALUES ($id, $project_id, $number, $name, $description, $task_order, $created_at, $updated_at);`);
|
|
18
|
+
|
|
19
|
+
return q.run({
|
|
20
|
+
$id: task.id,
|
|
21
|
+
$project_id: task.project_id,
|
|
22
|
+
$number: task.number,
|
|
23
|
+
$name: task.name,
|
|
24
|
+
$description: task.description,
|
|
25
|
+
$task_order: task.task_order,
|
|
26
|
+
$created_at: task.created_at,
|
|
27
|
+
$updated_at: task.updated_at,
|
|
28
|
+
});
|
|
29
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { DB_PATH, DB_TABLE } from "./constant";
|
|
3
|
-
import { randomUUIDv7 } from "bun";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
db.run("PRAGMA journal_mode = WAL;");
|
|
7
|
-
// TODO: https://bun.com/docs/runtime/sqlite#wal-sidecar-file-cleanup macOS does not auto cleanup
|
|
4
|
+
let db: Database | null = null;
|
|
8
5
|
|
|
9
|
-
|
|
6
|
+
function initDb() {
|
|
7
|
+
// TODO: turn on strict mode, and refactor `$` prefixes: https://bun.com/docs/runtime/sqlite#strict-true-lets-you-bind-values-without-prefixes
|
|
8
|
+
const db = new Database(DB_PATH, { create: true });
|
|
9
|
+
|
|
10
|
+
db.run("PRAGMA journal_mode = WAL;");
|
|
11
|
+
db.run("PRAGMA foreign_keys = true;");
|
|
12
|
+
// TODO: https://bun.com/docs/runtime/sqlite#wal-sidecar-file-cleanup macOS does not auto cleanup
|
|
13
|
+
|
|
14
|
+
// --- PROJECT TABLE
|
|
15
|
+
db.query(`CREATE TABLE IF NOT EXISTS ${DB_TABLE.project} (
|
|
10
16
|
id TEXT PRIMARY KEY,
|
|
11
17
|
key TEXT NOT NULL UNIQUE,
|
|
12
18
|
name TEXT NOT NULL,
|
|
19
|
+
task_count INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
created_at INTEGER NOT NULL,
|
|
21
|
+
updated_at INTEGER NOT NULL,
|
|
22
|
+
archived_at INTEGER);`).run();
|
|
23
|
+
|
|
24
|
+
// --- TASK TABLE
|
|
25
|
+
db.query(`CREATE TABLE IF NOT EXISTS ${DB_TABLE.task} (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
project_id TEXT NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
28
|
+
number INTEGER NOT NULL,
|
|
29
|
+
name TEXT NOT NULL,
|
|
30
|
+
description TEXT,
|
|
31
|
+
task_order TEXT,
|
|
13
32
|
created_at INTEGER NOT NULL,
|
|
14
33
|
updated_at INTEGER NOT NULL,
|
|
15
|
-
archived_at INTEGER);`);
|
|
16
|
-
projectTableSetup.run();
|
|
34
|
+
archived_at INTEGER);`).run();
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
return db.query(`SELECT * FROM ${DB_TABLE.project} WHERE key = $key`).get({ $key: key });
|
|
36
|
+
return db;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
export function addProject(key: string, name: string) {
|
|
24
|
-
const query = db.query(`INSERT INTO
|
|
25
|
-
${DB_TABLE.project} (id, key, name, created_at, updated_at)
|
|
26
|
-
VALUES ($id, $key, $name, $created, $updated);`);
|
|
27
|
-
const time = Date.now();
|
|
39
|
+
db = initDb();
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
export function getDb() {
|
|
42
|
+
if (db == null) {
|
|
43
|
+
console.error("There was a problem initializing the database");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
return db.query(`DELETE FROM ${DB_TABLE.project} WHERE key = $key;`).run({ $key: key });
|
|
47
|
+
return db;
|
|
34
48
|
}
|
package/src/lib.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
|
2
|
+
type BoundProperty<P extends string> = `$${P}`;
|
|
3
|
+
// https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
|
|
4
|
+
export type Bind<T, K extends string & keyof T = string & keyof T> = {
|
|
5
|
+
[Property in BoundProperty<K>]: T[keyof T];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type Project = {
|
|
9
|
+
id: string;
|
|
10
|
+
key: string;
|
|
11
|
+
name: string;
|
|
12
|
+
task_count: number;
|
|
13
|
+
created_at: number;
|
|
14
|
+
updated_at: number;
|
|
15
|
+
archived_at: number | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type Task = {
|
|
19
|
+
id: string;
|
|
20
|
+
project_id: string;
|
|
21
|
+
number: number;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string | null;
|
|
24
|
+
task_order: string;
|
|
25
|
+
created_at: number;
|
|
26
|
+
updated_at: number;
|
|
27
|
+
archived_at: number | null;
|
|
28
|
+
};
|