@zinn-dev/core 0.0.3 → 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 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 { db, addProject, getProjectByKey, deleteProjectByKey } from "./src/db";
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
- function standardizeKey(key: string) {
6
- // TODO: maybe force latin characters only to prevent unexpected stuff from charaters like Ğ, İ, etc.
7
- return key.toUpperCase();
8
- }
32
+ dbProject.deleteByKey(standardizedKey);
33
+ },
34
+ standardizeKey: standardizeProjectKey,
35
+ };
9
36
 
10
- // TODO: switch to object param
11
- export function createProject(key: string, name: string) {
12
- const standardizedKey = standardizeKey(key);
13
- const project = getProjectByKey(standardizedKey);
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
- if (project != null) {
16
- throw new Error(`Project with key "${standardizedKey}" already exists!`);
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
- addProject(standardizedKey, name);
20
- }
46
+ // TODO: do proper refined query
47
+ // FIXME: returns all tasks from *all* projects
48
+ const allTasks = dbTask.getAll();
21
49
 
22
- export function deleteProject(key: string) {
23
- const standardizedKey = standardizeKey(key);
24
- const project = getProjectByKey(standardizedKey);
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
- if (project == null) {
27
- throw new Error(`Project with key "${standardizedKey}" does not exist!`);
28
- }
56
+ const order = generateKeyBetween(previousTaskOrder, null);
29
57
 
30
- deleteProjectByKey(standardizedKey);
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",
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",
@@ -17,9 +17,13 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "index.ts"
20
+ "index.ts",
21
+ "src"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "fractional-indexing": "4.0.0"
24
28
  }
25
29
  }
package/src/config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { DATA_DIR, MAIN_DIR, CONFIG_PATH, defaultConfig } from "./constant";
3
+
4
+ if (!existsSync(MAIN_DIR)) {
5
+ mkdirSync(MAIN_DIR);
6
+ }
7
+
8
+ if (!existsSync(DATA_DIR)) {
9
+ mkdirSync(DATA_DIR);
10
+ }
11
+
12
+ if (!existsSync(CONFIG_PATH)) {
13
+ writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig) + "\n", { encoding: "utf-8" });
14
+ }
@@ -0,0 +1,19 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export const MAIN_FOLDER_NAME = ".zinn";
5
+ export const CONFIG_FILE_NAME = "settings.json";
6
+ export const DB_FILE_NAME = "zinn.sqlite";
7
+
8
+ export const MAIN_DIR = join(homedir(), MAIN_FOLDER_NAME);
9
+ export const CONFIG_PATH = join(MAIN_DIR, CONFIG_FILE_NAME);
10
+ export const DATA_DIR = join(MAIN_DIR, "data");
11
+ // TODO: environment variable based path overriding for tests
12
+ export const DB_PATH = join(DATA_DIR, DB_FILE_NAME);
13
+ export const DB_TABLE = {
14
+ project: "project",
15
+ task: "task",
16
+ };
17
+
18
+ // TODO: implement zod schema for setting.json
19
+ export const defaultConfig = {};
@@ -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 ADDED
@@ -0,0 +1,48 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { DB_PATH, DB_TABLE } from "./constant";
3
+
4
+ let db: Database | null = null;
5
+
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} (
16
+ id TEXT PRIMARY KEY,
17
+ key TEXT NOT NULL UNIQUE,
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,
32
+ created_at INTEGER NOT NULL,
33
+ updated_at INTEGER NOT NULL,
34
+ archived_at INTEGER);`).run();
35
+
36
+ return db;
37
+ }
38
+
39
+ db = initDb();
40
+
41
+ export function getDb() {
42
+ if (db == null) {
43
+ console.error("There was a problem initializing the database");
44
+ process.exit(1);
45
+ }
46
+
47
+ return db;
48
+ }
package/src/lib.ts ADDED
@@ -0,0 +1,4 @@
1
+ export function standardizeProjectKey(key: string) {
2
+ // TODO: maybe force latin characters only to prevent unexpected stuff from charaters like Ğ, İ, etc.
3
+ return key.toUpperCase();
4
+ }
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
+ };