@house-rules/projects 0.0.0-stage → 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.
@@ -0,0 +1,75 @@
1
+ import { Schema } from "effect";
2
+ export declare const Project: Schema.Struct<{
3
+ readonly id: Schema.NonEmptyString;
4
+ readonly slug: Schema.NonEmptyString;
5
+ readonly name: Schema.NonEmptyString;
6
+ readonly status: Schema.NonEmptyString;
7
+ readonly description: Schema.String;
8
+ readonly createdAt: Schema.Int;
9
+ readonly updatedAt: Schema.Int;
10
+ }>;
11
+ export type Project = typeof Project.Type;
12
+ export declare const ProjectRef: Schema.Union<readonly [Schema.Struct<{
13
+ readonly id: Schema.NonEmptyString;
14
+ }>, Schema.Struct<{
15
+ readonly slug: Schema.NonEmptyString;
16
+ }>]>;
17
+ export type ProjectRef = typeof ProjectRef.Type;
18
+ export declare const NewProject: Schema.Struct<{
19
+ readonly slug: Schema.String;
20
+ readonly name: Schema.String;
21
+ readonly status: Schema.optionalKey<Schema.String>;
22
+ readonly description: Schema.optionalKey<Schema.String>;
23
+ }>;
24
+ export type NewProject = typeof NewProject.Type;
25
+ export declare const ProjectPatch: Schema.Struct<{
26
+ readonly slug: Schema.optionalKey<Schema.String>;
27
+ readonly name: Schema.optionalKey<Schema.String>;
28
+ readonly status: Schema.optionalKey<Schema.String>;
29
+ readonly description: Schema.optionalKey<Schema.String>;
30
+ }>;
31
+ export type ProjectPatch = typeof ProjectPatch.Type;
32
+ export declare const ProjectFilter: Schema.Struct<{
33
+ readonly status: Schema.optionalKey<Schema.String>;
34
+ }>;
35
+ export type ProjectFilter = typeof ProjectFilter.Type;
36
+ declare const ProjectNotFound_base: Schema.Class<ProjectNotFound, Schema.TaggedStruct<"ProjectNotFound", {
37
+ readonly kind: Schema.tag<"not_found">;
38
+ }>, import("effect/Cause").YieldableError>;
39
+ export declare class ProjectNotFound extends ProjectNotFound_base {
40
+ }
41
+ declare const ProjectSlugTaken_base: Schema.Class<ProjectSlugTaken, Schema.TaggedStruct<"ProjectSlugTaken", {
42
+ readonly kind: Schema.tag<"conflict">;
43
+ readonly slug: Schema.String;
44
+ }>, import("effect/Cause").YieldableError>;
45
+ export declare class ProjectSlugTaken extends ProjectSlugTaken_base {
46
+ }
47
+ declare const ProjectInputInvalid_base: Schema.Class<ProjectInputInvalid, Schema.TaggedStruct<"ProjectInputInvalid", {
48
+ readonly kind: Schema.tag<"invalid">;
49
+ readonly field: Schema.Literals<readonly ["slug", "name", "status", "options"]>;
50
+ readonly message: Schema.String;
51
+ }>, import("effect/Cause").YieldableError>;
52
+ export declare class ProjectInputInvalid extends ProjectInputInvalid_base {
53
+ }
54
+ declare const ProjectAccessDenied_base: Schema.Class<ProjectAccessDenied, Schema.TaggedStruct<"ProjectAccessDenied", {
55
+ readonly kind: Schema.tag<"forbidden">;
56
+ }>, import("effect/Cause").YieldableError>;
57
+ export declare class ProjectAccessDenied extends ProjectAccessDenied_base {
58
+ }
59
+ declare const ProjectStoreUnavailable_base: Schema.Class<ProjectStoreUnavailable, Schema.TaggedStruct<"ProjectStoreUnavailable", {
60
+ readonly kind: Schema.tag<"unavailable">;
61
+ readonly cause: Schema.Defect;
62
+ }>, import("effect/Cause").YieldableError>;
63
+ export declare class ProjectStoreUnavailable extends ProjectStoreUnavailable_base {
64
+ get message(): string;
65
+ }
66
+ declare const ProjectAccessUnavailable_base: Schema.Class<ProjectAccessUnavailable, Schema.TaggedStruct<"ProjectAccessUnavailable", {
67
+ readonly kind: Schema.tag<"unavailable">;
68
+ readonly cause: Schema.Defect;
69
+ }>, import("effect/Cause").YieldableError>;
70
+ export declare class ProjectAccessUnavailable extends ProjectAccessUnavailable_base {
71
+ get message(): string;
72
+ }
73
+ export declare const ProjectFailure: Schema.Union<readonly [typeof ProjectNotFound, typeof ProjectSlugTaken, typeof ProjectInputInvalid, typeof ProjectAccessDenied, typeof ProjectStoreUnavailable, typeof ProjectAccessUnavailable]>;
74
+ export type ProjectFailure = typeof ProjectFailure.Type;
75
+ export {};
@@ -0,0 +1,71 @@
1
+ import { errorKind } from "@house-rules/domain/contract";
2
+ import { Schema } from "effect";
3
+ export const Project = Schema.Struct({
4
+ id: Schema.NonEmptyString,
5
+ slug: Schema.NonEmptyString,
6
+ name: Schema.NonEmptyString,
7
+ status: Schema.NonEmptyString,
8
+ description: Schema.String,
9
+ createdAt: Schema.Int,
10
+ updatedAt: Schema.Int,
11
+ });
12
+ export const ProjectRef = Schema.Union([
13
+ Schema.Struct({ id: Schema.NonEmptyString }),
14
+ Schema.Struct({ slug: Schema.NonEmptyString }),
15
+ ]);
16
+ export const NewProject = Schema.Struct({
17
+ slug: Schema.String,
18
+ name: Schema.String,
19
+ status: Schema.optionalKey(Schema.String),
20
+ description: Schema.optionalKey(Schema.String),
21
+ });
22
+ export const ProjectPatch = Schema.Struct({
23
+ slug: Schema.optionalKey(Schema.String),
24
+ name: Schema.optionalKey(Schema.String),
25
+ status: Schema.optionalKey(Schema.String),
26
+ description: Schema.optionalKey(Schema.String),
27
+ });
28
+ export const ProjectFilter = Schema.Struct({ status: Schema.optionalKey(Schema.String) });
29
+ export class ProjectNotFound extends Schema.TaggedError()("ProjectNotFound", {
30
+ kind: errorKind("not_found"),
31
+ }) {
32
+ }
33
+ export class ProjectSlugTaken extends Schema.TaggedError()("ProjectSlugTaken", {
34
+ kind: errorKind("conflict"),
35
+ slug: Schema.String,
36
+ }) {
37
+ }
38
+ export class ProjectInputInvalid extends Schema.TaggedError()("ProjectInputInvalid", {
39
+ kind: errorKind("invalid"),
40
+ field: Schema.Literals(["slug", "name", "status", "options"]),
41
+ message: Schema.String,
42
+ }) {
43
+ }
44
+ export class ProjectAccessDenied extends Schema.TaggedError()("ProjectAccessDenied", {
45
+ kind: errorKind("forbidden"),
46
+ }) {
47
+ }
48
+ export class ProjectStoreUnavailable extends Schema.TaggedError()("ProjectStoreUnavailable", {
49
+ kind: errorKind("unavailable"),
50
+ cause: Schema.Defect(),
51
+ }) {
52
+ get message() {
53
+ return "Projects are unavailable. Try again shortly.";
54
+ }
55
+ }
56
+ export class ProjectAccessUnavailable extends Schema.TaggedError()("ProjectAccessUnavailable", {
57
+ kind: errorKind("unavailable"),
58
+ cause: Schema.Defect(),
59
+ }) {
60
+ get message() {
61
+ return "Project access is unavailable. Try again shortly.";
62
+ }
63
+ }
64
+ export const ProjectFailure = Schema.Union([
65
+ ProjectNotFound,
66
+ ProjectSlugTaken,
67
+ ProjectInputInvalid,
68
+ ProjectAccessDenied,
69
+ ProjectStoreUnavailable,
70
+ ProjectAccessUnavailable,
71
+ ]);
@@ -0,0 +1,135 @@
1
+ import { type Actor } from "@house-rules/domain";
2
+ import { Effect } from "effect";
3
+ import type { ProjectStore } from "../storage.js";
4
+ import type { ProjectAccess } from "./access.js";
5
+ import type { ProjectsOptions } from "./options.js";
6
+ import { ProjectAccessDenied, ProjectInputInvalid, ProjectNotFound, type ProjectRef } from "./project.js";
7
+ export declare const makeProjects: (store: ProjectStore["Service"], access: ProjectAccess["Service"] | undefined, options: ProjectsOptions, pattern: RegExp) => {
8
+ create: (actor: {
9
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
10
+ }, input: {
11
+ readonly slug: string;
12
+ readonly name: string;
13
+ readonly status?: string;
14
+ readonly description?: string;
15
+ }) => Effect.Effect<{
16
+ project: {
17
+ readonly id: string;
18
+ readonly slug: string;
19
+ readonly name: string;
20
+ readonly status: string;
21
+ readonly description: string;
22
+ readonly createdAt: number;
23
+ readonly updatedAt: number;
24
+ };
25
+ events: {
26
+ kind: "ProjectCreated";
27
+ project: {
28
+ id: string;
29
+ slug: string;
30
+ name: string;
31
+ status: string;
32
+ description: string;
33
+ createdAt: number;
34
+ updatedAt: number;
35
+ };
36
+ }[];
37
+ }, import("@house-rules/domain").NoOpenUnit | ProjectAccessDenied | import("./project.js").ProjectAccessUnavailable | ProjectInputInvalid | ProjectNotFound | import("./project.js").ProjectSlugTaken | import("./project.js").ProjectStoreUnavailable, never>;
38
+ update: (actor: {
39
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
40
+ }, ref: {
41
+ readonly id: string;
42
+ } | {
43
+ readonly slug: string;
44
+ }, patch: {
45
+ readonly slug?: string;
46
+ readonly name?: string;
47
+ readonly status?: string;
48
+ readonly description?: string;
49
+ }) => Effect.Effect<{
50
+ project: {
51
+ readonly id: string;
52
+ readonly slug: string;
53
+ readonly name: string;
54
+ readonly status: string;
55
+ readonly description: string;
56
+ readonly createdAt: number;
57
+ readonly updatedAt: number;
58
+ };
59
+ events: {
60
+ kind: "ProjectUpdated";
61
+ before: {
62
+ id: string;
63
+ slug: string;
64
+ name: string;
65
+ status: string;
66
+ description: string;
67
+ createdAt: number;
68
+ updatedAt: number;
69
+ };
70
+ after: {
71
+ id: string;
72
+ slug: string;
73
+ name: string;
74
+ status: string;
75
+ description: string;
76
+ createdAt: number;
77
+ updatedAt: number;
78
+ };
79
+ }[];
80
+ }, import("@house-rules/domain").NoOpenUnit | ProjectAccessDenied | import("./project.js").ProjectAccessUnavailable | ProjectInputInvalid | ProjectNotFound | import("./project.js").ProjectSlugTaken | import("./project.js").ProjectStoreUnavailable, never>;
81
+ archive: (actor: Actor, ref: ProjectRef) => Effect.Effect<{
82
+ project: {
83
+ readonly id: string;
84
+ readonly slug: string;
85
+ readonly name: string;
86
+ readonly status: string;
87
+ readonly description: string;
88
+ readonly createdAt: number;
89
+ readonly updatedAt: number;
90
+ };
91
+ events: {
92
+ kind: "ProjectUpdated";
93
+ before: {
94
+ id: string;
95
+ slug: string;
96
+ name: string;
97
+ status: string;
98
+ description: string;
99
+ createdAt: number;
100
+ updatedAt: number;
101
+ };
102
+ after: {
103
+ id: string;
104
+ slug: string;
105
+ name: string;
106
+ status: string;
107
+ description: string;
108
+ createdAt: number;
109
+ updatedAt: number;
110
+ };
111
+ }[];
112
+ }, import("@house-rules/domain").NoOpenUnit | ProjectAccessDenied | import("./project.js").ProjectAccessUnavailable | ProjectInputInvalid | ProjectNotFound | import("./project.js").ProjectSlugTaken | import("./project.js").ProjectStoreUnavailable, never>;
113
+ show: (actor: Actor, ref: ProjectRef) => Effect.Effect<{
114
+ readonly id: string;
115
+ readonly slug: string;
116
+ readonly name: string;
117
+ readonly status: string;
118
+ readonly description: string;
119
+ readonly createdAt: number;
120
+ readonly updatedAt: number;
121
+ }, ProjectAccessDenied | import("./project.js").ProjectAccessUnavailable | ProjectNotFound | import("./project.js").ProjectStoreUnavailable, never>;
122
+ list: (actor: {
123
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
124
+ }, filter?: {
125
+ readonly status?: string;
126
+ } | undefined) => Effect.Effect<readonly {
127
+ readonly id: string;
128
+ readonly slug: string;
129
+ readonly name: string;
130
+ readonly status: string;
131
+ readonly description: string;
132
+ readonly createdAt: number;
133
+ readonly updatedAt: number;
134
+ }[], import("./project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project.js").ProjectStoreUnavailable, never>;
135
+ };
@@ -0,0 +1,98 @@
1
+ import { UnitOfWork } from "@house-rules/domain";
2
+ import { DateTime, Effect, Random, Schema } from "effect";
3
+ import { ProjectAccessDenied, ProjectInputInvalid, ProjectNotFound, } from "./project.js";
4
+ export const makeProjects = (store, access, options, pattern) => {
5
+ const decodeStatus = (status) => Schema.decodeEffect(Schema.Literals(options.statuses))(status).pipe(Effect.mapError(() => new ProjectInputInvalid({
6
+ field: "status",
7
+ message: "Status must be one of the configured project statuses.",
8
+ })));
9
+ const decodeName = (name) => Schema.decodeEffect(Schema.NonEmptyString.check(Schema.isMaxLength(200)))(name).pipe(Effect.mapError(() => new ProjectInputInvalid({
10
+ field: "name",
11
+ message: "Name must contain 1 to 200 characters.",
12
+ })));
13
+ const decodeSlug = (slug) => Schema.decodeEffect(Schema.NonEmptyString.check(Schema.isMaxLength(options.slug.maxLength), Schema.isPattern(pattern)))(slug).pipe(Effect.mapError(() => new ProjectInputInvalid({
14
+ field: "slug",
15
+ message: `Slug must contain 1 to ${options.slug.maxLength} characters and match the app's slug format.`,
16
+ })));
17
+ const slugOf = (slug) => {
18
+ const trimmed = options.slug.trim ? slug.trim() : slug;
19
+ return options.slug.lowercase ? trimmed.toLowerCase() : trimmed;
20
+ };
21
+ const requireAccess = Effect.fn("Projects.requireAccess")(function* (actor, id, write) {
22
+ if (access === undefined)
23
+ return;
24
+ const relation = yield* access.access(actor, id);
25
+ if (relation === "write" || (!write && relation === "read"))
26
+ return;
27
+ return yield* id === null ? new ProjectAccessDenied() : new ProjectNotFound();
28
+ });
29
+ const find = Effect.fn("Projects.find")(function* (actor, ref, write) {
30
+ const normalized = "slug" in ref ? { slug: slugOf(ref.slug) } : ref;
31
+ const project = yield* store.get(normalized);
32
+ if (project === undefined)
33
+ return yield* new ProjectNotFound();
34
+ yield* requireAccess(actor, project.id, write);
35
+ return project;
36
+ });
37
+ const change = Effect.fn("Projects.change")(function* (actor, ref, patch) {
38
+ yield* UnitOfWork.required;
39
+ yield* store.lock;
40
+ const before = yield* find(actor, ref, true);
41
+ const slug = yield* decodeSlug(slugOf(patch.slug ?? before.slug));
42
+ const name = yield* decodeName((patch.name ?? before.name).trim());
43
+ const status = yield* decodeStatus(patch.status ?? before.status);
44
+ const description = patch.description ?? before.description;
45
+ if (slug === before.slug &&
46
+ name === before.name &&
47
+ status === before.status &&
48
+ description === before.description) {
49
+ return { project: before, events: [] };
50
+ }
51
+ const after = {
52
+ ...before,
53
+ slug,
54
+ name,
55
+ status,
56
+ description,
57
+ updatedAt: DateTime.toEpochMillis(yield* DateTime.now),
58
+ };
59
+ yield* store.replace(after);
60
+ return {
61
+ project: after,
62
+ events: [{ kind: "ProjectUpdated", before: { ...before }, after: { ...after } }],
63
+ };
64
+ });
65
+ return {
66
+ create: Effect.fn("Projects.create")(function* (actor, input) {
67
+ yield* UnitOfWork.required;
68
+ yield* requireAccess(actor, null, true);
69
+ const slug = yield* decodeSlug(slugOf(input.slug));
70
+ const name = yield* decodeName(input.name.trim());
71
+ const status = yield* decodeStatus(input.status ?? options.defaultStatus);
72
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
73
+ const id = yield* Effect.all(Array.from({ length: 4 }, () => Random.nextIntBetween(0, 0xffff_ffff))).pipe(Effect.map((words) => words.map((word) => word.toString(16).padStart(8, "0")).join("")));
74
+ const project = {
75
+ id,
76
+ slug,
77
+ name,
78
+ status,
79
+ description: input.description ?? "",
80
+ createdAt: now,
81
+ updatedAt: now,
82
+ };
83
+ yield* store.insert(project);
84
+ return {
85
+ project,
86
+ events: [{ kind: "ProjectCreated", project: { ...project } }],
87
+ };
88
+ }),
89
+ update: change,
90
+ archive: (actor, ref) => change(actor, ref, { status: options.archivedStatus }),
91
+ show: (actor, ref) => find(actor, ref, false),
92
+ list: Effect.fn("Projects.list")(function* (actor, filter = {}) {
93
+ const status = filter.status === undefined ? undefined : yield* decodeStatus(filter.status);
94
+ const ids = access === undefined ? undefined : yield* access.visibleProjects(actor);
95
+ return yield* store.list(status, ids);
96
+ }),
97
+ };
98
+ };
@@ -0,0 +1,13 @@
1
+ import type { NoOpenUnit } from "@house-rules/domain";
2
+ import { Context, type Effect } from "effect";
3
+ import type { Project, ProjectRef, ProjectSlugTaken, ProjectStoreUnavailable } from "./project/project.js";
4
+ declare const ProjectStore_base: Context.ServiceClass<ProjectStore, "@house-rules/projects/ProjectStore", {
5
+ readonly lock: Effect.Effect<void, ProjectStoreUnavailable | NoOpenUnit>;
6
+ readonly get: (ref: ProjectRef) => Effect.Effect<Project | undefined, ProjectStoreUnavailable>;
7
+ readonly list: (status?: string, visibleIds?: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<Project>, ProjectStoreUnavailable>;
8
+ readonly insert: (project: Project) => Effect.Effect<void, ProjectStoreUnavailable | ProjectSlugTaken | NoOpenUnit>;
9
+ readonly replace: (project: Project) => Effect.Effect<void, ProjectStoreUnavailable | ProjectSlugTaken | NoOpenUnit>;
10
+ }>;
11
+ export declare class ProjectStore extends ProjectStore_base {
12
+ }
13
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ export class ProjectStore extends Context.Service()("@house-rules/projects/ProjectStore") {
3
+ }
@@ -0,0 +1,9 @@
1
+ CREATE TABLE projects (
2
+ id text PRIMARY KEY,
3
+ slug text NOT NULL UNIQUE,
4
+ name text NOT NULL,
5
+ status text NOT NULL,
6
+ description text NOT NULL,
7
+ created_at bigint NOT NULL,
8
+ updated_at bigint NOT NULL
9
+ );
@@ -0,0 +1,2 @@
1
+ CREATE VIEW projects_view AS
2
+ SELECT id, slug, name, status, description, created_at, updated_at FROM projects;
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@house-rules/projects",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Configurable project records with unique slugs and returned facts.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "migrations",
10
+ "AGENTS.md",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./contract": {
19
+ "types": "./dist/contract.d.ts",
20
+ "default": "./dist/contract.js"
21
+ }
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/JakubSzwajka/house-rules.git",
29
+ "directory": "packages/projects"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/JakubSzwajka/house-rules/issues"
33
+ },
34
+ "homepage": "https://github.com/JakubSzwajka/house-rules",
35
+ "houseRules": {
36
+ "migrations": "migrations"
37
+ },
38
+ "peerDependencies": {
39
+ "@house-rules/application": "0.2.0",
40
+ "@house-rules/domain": "0.2.0",
41
+ "effect": "4.0.3"
42
+ },
43
+ "devDependencies": {
44
+ "@effect/platform-node": "4.0.3",
45
+ "@effect/platform-node-shared": "4.0.3",
46
+ "@effect/sql-pg": "4.0.3",
47
+ "@effect/vitest": "4.0.3",
48
+ "@house-rules/application": "0.2.0",
49
+ "@house-rules/domain": "0.2.0",
50
+ "@house-rules/migrations": "0.4.0",
51
+ "effect": "4.0.3",
52
+ "typescript": "7.0.2",
53
+ "vitest": "5.0.1"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc --project tsconfig.build.json",
57
+ "typecheck": "tsc --project tsconfig.json",
58
+ "test": "varlock run --path ../../ -- vitest run"
59
+ }
6
60
  }