@zinn-dev/core 0.2.0 → 0.5.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2026 Zinn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Column } from "./src/types";
2
+ import type { ProjectCreateInput, ProjectEditInput } from "./src/schemas/project";
3
+ import type { TaskCreateInput, TaskEditInput } from "./src/schemas/task";
2
4
 
3
5
  import { randomUUIDv7 } from "bun";
4
6
  import { generateKeyBetween } from "fractional-indexing";
@@ -8,33 +10,32 @@ import * as dbColumn from "./src/db/column";
8
10
  import * as dbTask from "./src/db/task";
9
11
  import { standardizeProjectKey } from "./src/lib";
10
12
  import { validateTaskInput, taskCreateSchema, taskEditSchema } from "./src/schemas/task";
11
- import type { TaskCreateInput, TaskEditInput } from "./src/schemas/task";
12
13
 
13
- export { taskCreateSchema, taskEditSchema } from "./src/schemas/task";
14
+ import {
15
+ validateProjectInput,
16
+ projectCreateSchema,
17
+ projectEditSchema,
18
+ } from "./src/schemas/project";
19
+
20
+ export type { ProjectCreateInput, ProjectEditInput } from "./src/schemas/project";
14
21
  export type { TaskCreateInput, TaskEditInput } from "./src/schemas/task";
15
22
 
23
+ export { projectCreateSchema, projectEditSchema } from "./src/schemas/project";
24
+ export { taskCreateSchema, taskEditSchema } from "./src/schemas/task";
25
+
16
26
  export const project = {
17
27
  getById: dbProject.getById,
18
28
  getByKey: dbProject.getByKey,
19
- create: (props: { key: string; name: string }) => {
20
- const validKeyRegex = /^[A-Za-z][A-Za-z0-9]*$/;
21
- if (!validKeyRegex.test(props.key)) {
22
- throw new Error("Project key must start with a letter and contain only letters and numbers");
23
- }
24
-
25
- const invalidNameCharRegex = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
26
- // control characters and Unicode line separators are not valid project data.
27
- if (props.name.trim().length === 0 || invalidNameCharRegex.test(props.name)) {
28
- throw new Error("Project name must contain printable text on a single line");
29
- }
30
-
29
+ create: (input: ProjectCreateInput) => {
30
+ const props = validateProjectInput(projectCreateSchema, input);
31
31
  const existingProject = dbProject.getByKey(props.key);
32
32
 
33
33
  if (existingProject != null) {
34
34
  throw new Error(`Project with key "${existingProject.key}" already exists!`);
35
35
  }
36
36
 
37
- const project = dbProject.create(props)!;
37
+ const key = standardizeProjectKey(props.key);
38
+ const project = dbProject.create({ key, name: props.name ?? key })!;
38
39
 
39
40
  const defaultColumns = ["Backlog", "TODO", "In Progress", "Review", "Done"];
40
41
  let lastColumnOrder: string | null = null;
@@ -52,6 +53,23 @@ export const project = {
52
53
 
53
54
  return project;
54
55
  },
56
+ /** Rename a project. Same name does not alter the timestamp */
57
+ edit: (input: ProjectEditInput) => {
58
+ const props = validateProjectInput(projectEditSchema, input);
59
+ const projectMatch = dbProject.getByKey(props.projectKey);
60
+
61
+ if (projectMatch == null) {
62
+ throw new Error(
63
+ `Project with key "${standardizeProjectKey(props.projectKey)}" does not exist!`,
64
+ );
65
+ }
66
+
67
+ if (props.name === projectMatch.name) {
68
+ return projectMatch;
69
+ }
70
+
71
+ return dbProject.update({ id: projectMatch.id, name: props.name, updated_at: Date.now() })!;
72
+ },
55
73
  getAll: dbProject.getAll,
56
74
  delete: (key: string) => {
57
75
  const project = dbProject.getByKey(key);
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@zinn-dev/core",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared logic for zinn, a kanban workflow in the terminal.",
5
+ "license": "MIT",
6
+ "bugs": {
7
+ "url": "https://github.com/yethranayeh/zinn/issues"
8
+ },
9
+ "author": {
10
+ "name": "Alper Halil",
11
+ "email": "contact@aktasalper.com",
12
+ "url": "https://aktasalper.com"
13
+ },
5
14
  "repository": {
6
15
  "type": "git",
7
16
  "url": "git+https://github.com/yethranayeh/zinn.git",
@@ -18,7 +27,8 @@
18
27
  },
19
28
  "files": [
20
29
  "index.ts",
21
- "src"
30
+ "src",
31
+ "LICENSE"
22
32
  ],
23
33
  "publishConfig": {
24
34
  "access": "public"
package/src/db/project.ts CHANGED
@@ -71,3 +71,13 @@ export function deleteByKey(key: string) {
71
71
  .query(`DELETE FROM ${DB_TABLE.project} WHERE key = $key;`)
72
72
  .run({ $key: standardizedKey });
73
73
  }
74
+
75
+ export function update(props: Pick<Project, "id" | "name" | "updated_at">) {
76
+ const db = getDb();
77
+
78
+ return db
79
+ .query<Project, Bind<typeof props>>(`UPDATE ${DB_TABLE.project}
80
+ SET name = $name, updated_at = $updated_at
81
+ WHERE id = $id RETURNING *`)
82
+ .get({ $id: props.id, $name: props.name, $updated_at: props.updated_at });
83
+ }
@@ -0,0 +1,29 @@
1
+ import { expect, test } from "bun:test";
2
+ import { projectCreateSchema, projectEditSchema, validateProjectInput } from "./project";
3
+
4
+ test("project creation accepts an omitted name but rejects null", () => {
5
+ expect(projectCreateSchema.parse({ key: "test" })).toEqual({ key: "test" });
6
+ expect(projectCreateSchema.safeParse({ key: "TEST", name: null }).success).toBe(false);
7
+ });
8
+
9
+ test("project creation and editing share name validation and preserve supplied text", () => {
10
+ for (const name of ["", " ", "Two\nlines", "Bad\u0000name"]) {
11
+ expect(projectCreateSchema.safeParse({ key: "TEST", name }).success).toBe(false);
12
+ expect(projectEditSchema.safeParse({ projectKey: "TEST", name }).success).toBe(false);
13
+ }
14
+ expect(projectEditSchema.parse({ projectKey: "test", name: " Name " })).toEqual({
15
+ projectKey: "test", name: " Name ",
16
+ });
17
+ });
18
+
19
+ test("project edits require a name and reject unsupported fields", () => {
20
+ for (const input of [
21
+ { projectKey: "TEST" },
22
+ { projectKey: "TEST", name: undefined },
23
+ { projectKey: "TEST", name: null },
24
+ { projectKey: "TEST", name: 42 },
25
+ { projectKey: "TEST", name: "Name", key: "NEW" },
26
+ ]) {
27
+ expect(() => validateProjectInput(projectEditSchema, input)).toThrow();
28
+ }
29
+ });
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+
3
+ const nameMessage = "Project name must contain printable text on a single line";
4
+ const invalidNameCharRegex = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
5
+
6
+ export const projectNameSchema = z
7
+ .string({ error: nameMessage })
8
+ .refine((value) => value.trim().length > 0 && !invalidNameCharRegex.test(value), nameMessage);
9
+
10
+ export const projectCreateSchema = z.strictObject({
11
+ key: z
12
+ .string()
13
+ .regex(
14
+ /^[A-Za-z][A-Za-z0-9]*$/,
15
+ "Project key must start with a letter and contain only letters and numbers",
16
+ ),
17
+ name: projectNameSchema.optional(),
18
+ });
19
+
20
+ export const projectEditSchema = z.strictObject({
21
+ projectKey: z.string(),
22
+ name: projectNameSchema,
23
+ });
24
+
25
+ export type ProjectCreateInput = z.infer<typeof projectCreateSchema>;
26
+ export type ProjectEditInput = z.infer<typeof projectEditSchema>;
27
+
28
+ export function validateProjectInput<S extends z.ZodType>(schema: S, input: unknown): z.output<S> {
29
+ const result = schema.safeParse(input);
30
+ if (!result.success) {
31
+ throw new Error(result.error.issues.map((issue) => issue.message).join("\n"));
32
+ }
33
+
34
+ return result.data;
35
+ }