@zinn-dev/core 0.2.0 → 0.3.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/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,26 +10,24 @@ 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) {
@@ -52,6 +52,23 @@ export const project = {
52
52
 
53
53
  return project;
54
54
  },
55
+ /** Rename a project. Same name does not alter the timestamp */
56
+ edit: (input: ProjectEditInput) => {
57
+ const props = validateProjectInput(projectEditSchema, input);
58
+ const projectMatch = dbProject.getByKey(props.projectKey);
59
+
60
+ if (projectMatch == null) {
61
+ throw new Error(
62
+ `Project with key "${standardizeProjectKey(props.projectKey)}" does not exist!`,
63
+ );
64
+ }
65
+
66
+ if (props.name === projectMatch.name) {
67
+ return projectMatch;
68
+ }
69
+
70
+ return dbProject.update({ id: projectMatch.id, name: props.name, updated_at: Date.now() })!;
71
+ },
55
72
  getAll: dbProject.getAll,
56
73
  delete: (key: string) => {
57
74
  const project = dbProject.getByKey(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zinn-dev/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared logic for zinn, a kanban workflow in the terminal.",
5
5
  "repository": {
6
6
  "type": "git",
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,24 @@
1
+ import { expect, test } from "bun:test";
2
+ import { projectCreateSchema, projectEditSchema, validateProjectInput } from "./project";
3
+
4
+ test("project creation and editing share name validation and preserve supplied text", () => {
5
+ for (const name of ["", " ", "Two\nlines", "Bad\u0000name"]) {
6
+ expect(projectCreateSchema.safeParse({ key: "TEST", name }).success).toBe(false);
7
+ expect(projectEditSchema.safeParse({ projectKey: "TEST", name }).success).toBe(false);
8
+ }
9
+ expect(projectEditSchema.parse({ projectKey: "test", name: " Name " })).toEqual({
10
+ projectKey: "test", name: " Name ",
11
+ });
12
+ });
13
+
14
+ test("project edits require a name and reject unsupported fields", () => {
15
+ for (const input of [
16
+ { projectKey: "TEST" },
17
+ { projectKey: "TEST", name: undefined },
18
+ { projectKey: "TEST", name: null },
19
+ { projectKey: "TEST", name: 42 },
20
+ { projectKey: "TEST", name: "Name", key: "NEW" },
21
+ ]) {
22
+ expect(() => validateProjectInput(projectEditSchema, input)).toThrow();
23
+ }
24
+ });
@@ -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,
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
+ }