@house-rules/projects 0.0.0-stage → 0.1.1
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/AGENTS.md +17 -0
- package/LICENSE +21 -0
- package/README.md +41 -2
- package/dist/adapters/memory/project.d.ts +3 -0
- package/dist/adapters/memory/project.js +40 -0
- package/dist/adapters/postgres/project.d.ts +4 -0
- package/dist/adapters/postgres/project.js +42 -0
- package/dist/contract.d.ts +4 -0
- package/dist/contract.js +4 -0
- package/dist/facade.d.ts +304 -0
- package/dist/facade.js +28 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +17 -0
- package/dist/project/access.d.ts +10 -0
- package/dist/project/access.js +3 -0
- package/dist/project/bind-projects.d.ts +3 -0
- package/dist/project/bind-projects.js +21 -0
- package/dist/project/events.d.ts +93 -0
- package/dist/project/events.js +17 -0
- package/dist/project/operations.d.ts +135 -0
- package/dist/project/operations.js +51 -0
- package/dist/project/options.d.ts +26 -0
- package/dist/project/options.js +20 -0
- package/dist/project/order.d.ts +2 -0
- package/dist/project/order.js +2 -0
- package/dist/project/permissions.d.ts +5 -0
- package/dist/project/permissions.js +1 -0
- package/dist/project/project.d.ts +75 -0
- package/dist/project/project.js +71 -0
- package/dist/project/projects.d.ts +135 -0
- package/dist/project/projects.js +98 -0
- package/dist/storage.d.ts +13 -0
- package/dist/storage.js +3 -0
- package/migrations/0001_projects.sql +9 -0
- package/migrations/0002_projects_view.sql +2 -0
- package/package.json +57 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @house-rules/projects: notes for agents
|
|
2
|
+
|
|
3
|
+
Projects is a capability module owning project records, not tasks, repositories or notes. Read `README.md` for options and adoption differences.
|
|
4
|
+
|
|
5
|
+
## Mount and bind
|
|
6
|
+
|
|
7
|
+
Use `Projects.layerPostgres(options)` with the shared SQL client, or `Projects.layerMemory(options)` with the memory unit adapter. Raw `Projects.layer(options)` needs `ProjectStore`. Storage statics default to `Projects.presets.default`; raw Projects.layer requires options. Add `{ ...ProjectsMount, layer }` to the module plan. Composition captures Projects and calls `bindProjects` once through Runtime. Features call `ProjectsOperations.create/update/show/list/archive` through declared edges.
|
|
8
|
+
|
|
9
|
+
Register `{ "workspace": "packages/projects" }` in migrations.json. Installed consumers use `{ "package": "@house-rules/projects" }`. Migrations own `projects` and publish `projects_view` over that table alone. The view exposes id, slug, name, status, description, created_at and updated_at. Read models must filter access themselves.
|
|
10
|
+
|
|
11
|
+
## Access and facts
|
|
12
|
+
|
|
13
|
+
Person callers need `projects:read` or `projects:write`. All operation bindings refuse system callers with ProjectAccessDenied, even when system permissions include those grants. No ownership is stored or inferred. Without ProjectAccess, every permitted person can access every project. Composition may provide ProjectAccess before acquiring Projects. Its access method takes the actor and project id, or null for creation, and returns write/read/none. Its visibleProjects method returns ids for storage filtering. Lists trust that id set. Denied existing objects appear not found; denied creation is forbidden. Port failures use ProjectAccessUnavailable.
|
|
14
|
+
|
|
15
|
+
Writes return `{ project, events }`. ProjectCreated carries the stored project. ProjectUpdated carries before and after, including slug changes and archiving. No-op changes return the unchanged project and no events. Import ProjectEvents from the browser-safe contract door. The composing layer decides whether to record facts. Projects never emits events or reads CurrentActor.
|
|
16
|
+
|
|
17
|
+
Writes require an open unit and never open one. Memory registers undo before mutation. Postgres serializes module writes with a transaction advisory lock, then joins the open transaction. Do not remove the lock without preserving atomic slug conflicts and read/modify/write behaviour. Invalid input returns ProjectInputInvalid with field slug/name/status and a safe user-facing message; invalid configuration uses field options. Unavailable errors expose fixed text, never their cause.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Szwajka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @house-rules/projects
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Project records with configurable statuses and unique slugs. This support module is not in the public publish workflow.
|
|
4
|
+
|
|
5
|
+
## Mount
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const projects = Projects.layerPostgres(Projects.presets.default);
|
|
9
|
+
const mount = { ...ProjectsMount, layer: projects };
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Provide the shared SQL client and sqlUnitOfWork in the module plan's infrastructure. Use Projects.layerMemory with memoryUnitOfWork for memory storage. Register packages/projects in migrations.json, or the package identity when installed. Composition binds operations through bindProjects once. Features call ProjectsOperations through declared edges; write operations lift transactional.
|
|
13
|
+
|
|
14
|
+
## Options
|
|
15
|
+
|
|
16
|
+
Projects.presets.default supplies statuses active/archived, defaultStatus active and archivedStatus archived. Configuration must list distinct, nonempty statuses and contain both chosen statuses. Statuses are exact, case-sensitive strings. Set all options when overriding the preset, or spread it to change selected values.
|
|
17
|
+
|
|
18
|
+
The default slug rules trim and lowercase input, require a letter or digit first, then lowercase letters, digits or hyphens, and cap length at 64 UTF-16 units. Configure slug.pattern as a regular-expression source, slug.maxLength, slug.lowercase and slug.trim. The pattern applies after normalisation; use anchors for whole-slug rules. Invalid regex syntax fails acquisition as ProjectInputInvalid. Slugs remain unique regardless of status. Names are trimmed and require 1 to 200 UTF-16 units. Descriptions are plain text, defaulting to an empty string. Invalid slug, name or status returns ProjectInputInvalid with the field and a safe user-facing message. Invalid configuration uses field options.
|
|
19
|
+
|
|
20
|
+
## Operations and facts
|
|
21
|
+
|
|
22
|
+
| Operation | Input | Result |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| create | slug, name, optional status and description | project plus ProjectCreated fact |
|
|
25
|
+
| update | ref and patch with optional slug/name/status/description | project plus ProjectUpdated fact when changed |
|
|
26
|
+
| show | `{ id }` or `{ slug }` | project |
|
|
27
|
+
| list | optional status | projects ordered by lowercased name, then slug |
|
|
28
|
+
| archive | `{ id }` or `{ slug }` | update result using archivedStatus |
|
|
29
|
+
|
|
30
|
+
List without a status includes all projects, including archived records. Apps must filter the statuses they want to show. Slug changes are allowed; apps that forbid them must enforce that rule in their features. Explicit status filters are validated. Ordering uses JavaScript lowercase and code-unit comparison in both adapters, independent of database collation. Archive is a status change, not a delete. A repeated archive or unchanged patch returns the existing project with an empty events array and leaves updatedAt unchanged. Timestamps are epoch milliseconds. Facts contain plain project snapshots without envelopes; a feature may record them through its own event-log edge.
|
|
31
|
+
|
|
32
|
+
## Access
|
|
33
|
+
|
|
34
|
+
Person operation callers need projects:read or projects:write. All bindings refuse system callers, even with those grants. The raw service accepts an explicit Actor and assumes the composing layer has checked permissions. Without ProjectAccess there is no object access rule. To restrict objects, provide ProjectAccess when acquiring Projects. access(actor, projectId) returns write/read/none; null checks creation. visibleProjects(actor) returns ids to filter lists in storage, including SQL. A read relation cannot write. Denied reads or changes return ProjectNotFound; denied creation returns ProjectAccessDenied. Map host failures to ProjectAccessUnavailable. The port belongs to Projects and stores no host policy.
|
|
35
|
+
|
|
36
|
+
## Storage
|
|
37
|
+
|
|
38
|
+
Both adapters require an open unit for writes and support rollback. Postgres serializes module mutations with a transaction-scoped advisory lock to preserve rename uniqueness and prevent lost field updates. Seed scripts and other direct writers must take the same `pg_advisory_xact_lock(hashtext('house-rules/projects'))` in their transaction before checking or writing slugs. Otherwise, concurrent duplicates can return ProjectStoreUnavailable rather than ProjectSlugTaken. It decodes rows through Schema. The projects_view publishes only this module's columns. Consumers must apply their own access filters when selecting that view. Run the shared memory/Postgres suites with `pnpm --filter @house-rules/projects test`; database tests use MigrationsTesting.database.
|
|
39
|
+
|
|
40
|
+
## Board adoption
|
|
41
|
+
|
|
42
|
+
Configure active/paused/done/archived and the board slug pattern `^[a-z0-9][a-z0-9_-]{0,63}$`, with lowercase and trim disabled, to keep board's strict slug acceptance. The default framework rules instead normalise case/space and reject underscores. Board's list hides done/archived by default; this list includes every status unless one status is requested. Board's ordering uses PostgreSQL lower(name) and its collation; this module uses deterministic JavaScript comparison. Board's no-op update is null; here it is a project with no facts. Records expose a stable text id and epoch-millisecond createdAt/updatedAt instead of board's createdAt string and hidden UUID. Slug changes are supported. Joined task counts, repositories, tasks and notes remain app read-model work. Board keeps owner/agent writes in composition's access port. No board files are changed by this package.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { UnitOfWork } from "@house-rules/domain";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { ProjectStore } from "../../storage.js";
|
|
4
|
+
import { ProjectSlugTaken, ProjectStoreUnavailable } from "../../project/project.js";
|
|
5
|
+
import { projectOrder } from "../../project/order.js";
|
|
6
|
+
export const memoryProjectStore = () => Layer.sync(ProjectStore, () => {
|
|
7
|
+
const rows = new Map();
|
|
8
|
+
const save = Effect.fn("ProjectStore.memory.save")(function* (project, insert) {
|
|
9
|
+
yield* UnitOfWork.required;
|
|
10
|
+
if ([...rows.values()].some((row) => row.slug === project.slug && row.id !== project.id)) {
|
|
11
|
+
return yield* new ProjectSlugTaken({ slug: project.slug });
|
|
12
|
+
}
|
|
13
|
+
const before = rows.get(project.id);
|
|
14
|
+
if (insert === (before !== undefined))
|
|
15
|
+
return yield* new ProjectStoreUnavailable({ cause: "Invalid project id." });
|
|
16
|
+
yield* UnitOfWork.onRollback(Effect.sync(() => {
|
|
17
|
+
if (before === undefined)
|
|
18
|
+
rows.delete(project.id);
|
|
19
|
+
else
|
|
20
|
+
rows.set(project.id, before);
|
|
21
|
+
}));
|
|
22
|
+
rows.set(project.id, { ...project });
|
|
23
|
+
});
|
|
24
|
+
return ProjectStore.of({
|
|
25
|
+
lock: UnitOfWork.required,
|
|
26
|
+
get: (ref) => Effect.sync(() => {
|
|
27
|
+
const found = "id" in ref
|
|
28
|
+
? rows.get(ref.id)
|
|
29
|
+
: [...rows.values()].find((row) => row.slug === ref.slug);
|
|
30
|
+
return found === undefined ? undefined : { ...found };
|
|
31
|
+
}),
|
|
32
|
+
list: (status, ids) => Effect.sync(() => [...rows.values()]
|
|
33
|
+
.filter((row) => (status === undefined || row.status === status) &&
|
|
34
|
+
(ids === undefined || ids.includes(row.id)))
|
|
35
|
+
.sort(projectOrder)
|
|
36
|
+
.map((row) => ({ ...row }))),
|
|
37
|
+
insert: (project) => save(project, true),
|
|
38
|
+
replace: (project) => save(project, false),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { UnitOfWork } from "@house-rules/domain";
|
|
2
|
+
import { Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
4
|
+
import { ProjectStore } from "../../storage.js";
|
|
5
|
+
import { Project, ProjectSlugTaken, ProjectStoreUnavailable } from "../../project/project.js";
|
|
6
|
+
import { projectOrder } from "../../project/order.js";
|
|
7
|
+
const decodeRows = Schema.decodeUnknownEffect(Schema.Array(Project));
|
|
8
|
+
const unavailable = (cause) => new ProjectStoreUnavailable({ cause });
|
|
9
|
+
export const postgresProjectStore = Layer.effect(ProjectStore, Effect.gen(function* () {
|
|
10
|
+
const sql = yield* SqlClient;
|
|
11
|
+
const columns = sql.literal('id, slug, name, status, description, created_at::float8 as "createdAt", updated_at::float8 as "updatedAt"');
|
|
12
|
+
const read = (query) => query.pipe(Effect.flatMap(decodeRows), Effect.mapError(unavailable));
|
|
13
|
+
// One transaction lock keeps rename conflicts and read/modify/write changes atomic.
|
|
14
|
+
const lock = Effect.gen(function* () {
|
|
15
|
+
yield* UnitOfWork.required;
|
|
16
|
+
yield* sql `select pg_advisory_xact_lock(hashtext('house-rules/projects'))`.pipe(Effect.mapError(unavailable));
|
|
17
|
+
});
|
|
18
|
+
const save = Effect.fn("ProjectStore.postgres.save")(function* (project, insert) {
|
|
19
|
+
yield* lock;
|
|
20
|
+
const taken = yield* sql `select id from projects where slug = ${project.slug} and id <> ${project.id}`.pipe(Effect.mapError(unavailable));
|
|
21
|
+
if (taken.length > 0)
|
|
22
|
+
return yield* new ProjectSlugTaken({ slug: project.slug });
|
|
23
|
+
const rows = yield* read(insert
|
|
24
|
+
? sql `insert into projects (id, slug, name, status, description, created_at, updated_at) values (${project.id}, ${project.slug}, ${project.name}, ${project.status}, ${project.description}, ${project.createdAt}, ${project.updatedAt}) on conflict (slug) do nothing returning ${columns}`
|
|
25
|
+
: sql `update projects set slug = ${project.slug}, name = ${project.name}, status = ${project.status}, description = ${project.description}, updated_at = ${project.updatedAt} where id = ${project.id} returning ${columns}`);
|
|
26
|
+
if (rows.length === 0)
|
|
27
|
+
return yield* new ProjectStoreUnavailable({ cause: "Project write returned no row." });
|
|
28
|
+
});
|
|
29
|
+
return ProjectStore.of({
|
|
30
|
+
lock,
|
|
31
|
+
get: (ref) => read("id" in ref
|
|
32
|
+
? sql `select ${columns} from projects where id = ${ref.id}`
|
|
33
|
+
: sql `select ${columns} from projects where slug = ${ref.slug}`).pipe(Effect.map((rows) => rows[0])),
|
|
34
|
+
list: (status, ids) => {
|
|
35
|
+
if (ids !== undefined && ids.length === 0)
|
|
36
|
+
return Effect.succeed([]);
|
|
37
|
+
return read(sql `select ${columns} from projects where ${status === undefined ? sql `true` : sql `status = ${status}`} and ${ids === undefined ? sql `true` : sql `id in ${sql.in(ids)}`}`).pipe(Effect.map((rows) => [...rows].sort(projectOrder)));
|
|
38
|
+
},
|
|
39
|
+
insert: (project) => save(project, true),
|
|
40
|
+
replace: (project) => save(project, false),
|
|
41
|
+
});
|
|
42
|
+
}));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CreateProjectResult, UpdateProjectResult, ProjectEvents } from "./project/events.js";
|
|
2
|
+
export { ProjectsOperations } from "./project/operations.js";
|
|
3
|
+
export { ProjectPermissions, type ProjectPermission } from "./project/permissions.js";
|
|
4
|
+
export { Project, ProjectRef, NewProject, ProjectPatch, ProjectFilter, ProjectFailure, ProjectNotFound, ProjectSlugTaken, ProjectInputInvalid, ProjectAccessDenied, ProjectAccessUnavailable, ProjectStoreUnavailable, } from "./project/project.js";
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CreateProjectResult, UpdateProjectResult, ProjectEvents } from "./project/events.js";
|
|
2
|
+
export { ProjectsOperations } from "./project/operations.js";
|
|
3
|
+
export { ProjectPermissions } from "./project/permissions.js";
|
|
4
|
+
export { Project, ProjectRef, NewProject, ProjectPatch, ProjectFilter, ProjectFailure, ProjectNotFound, ProjectSlugTaken, ProjectInputInvalid, ProjectAccessDenied, ProjectAccessUnavailable, ProjectStoreUnavailable, } from "./project/project.js";
|
package/dist/facade.d.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { ProjectStore } from "./storage.js";
|
|
3
|
+
import { ProjectsOptions } from "./project/options.js";
|
|
4
|
+
import { ProjectInputInvalid } from "./project/project.js";
|
|
5
|
+
declare const Projects_base: import("effect/Context").ServiceClass<Projects, "projects", {
|
|
6
|
+
create: (actor: {
|
|
7
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
8
|
+
}, input: {
|
|
9
|
+
readonly slug: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly status?: string;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
}) => Effect.Effect<{
|
|
14
|
+
project: {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly slug: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly status: string;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
readonly createdAt: number;
|
|
21
|
+
readonly updatedAt: number;
|
|
22
|
+
};
|
|
23
|
+
events: {
|
|
24
|
+
kind: "ProjectCreated";
|
|
25
|
+
project: {
|
|
26
|
+
id: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
name: string;
|
|
29
|
+
status: string;
|
|
30
|
+
description: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
updatedAt: number;
|
|
33
|
+
};
|
|
34
|
+
}[];
|
|
35
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
36
|
+
update: (actor: {
|
|
37
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
38
|
+
}, ref: {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly slug: string;
|
|
42
|
+
}, patch: {
|
|
43
|
+
readonly slug?: string;
|
|
44
|
+
readonly name?: string;
|
|
45
|
+
readonly status?: string;
|
|
46
|
+
readonly description?: string;
|
|
47
|
+
}) => Effect.Effect<{
|
|
48
|
+
project: {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly slug: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly status: string;
|
|
53
|
+
readonly description: string;
|
|
54
|
+
readonly createdAt: number;
|
|
55
|
+
readonly updatedAt: number;
|
|
56
|
+
};
|
|
57
|
+
events: {
|
|
58
|
+
kind: "ProjectUpdated";
|
|
59
|
+
before: {
|
|
60
|
+
id: string;
|
|
61
|
+
slug: string;
|
|
62
|
+
name: string;
|
|
63
|
+
status: string;
|
|
64
|
+
description: string;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
};
|
|
68
|
+
after: {
|
|
69
|
+
id: string;
|
|
70
|
+
slug: string;
|
|
71
|
+
name: string;
|
|
72
|
+
status: string;
|
|
73
|
+
description: string;
|
|
74
|
+
createdAt: number;
|
|
75
|
+
updatedAt: number;
|
|
76
|
+
};
|
|
77
|
+
}[];
|
|
78
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
79
|
+
archive: (actor: import("@house-rules/domain").Actor, ref: import("./project/project.js").ProjectRef) => Effect.Effect<{
|
|
80
|
+
project: {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly slug: string;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly status: string;
|
|
85
|
+
readonly description: string;
|
|
86
|
+
readonly createdAt: number;
|
|
87
|
+
readonly updatedAt: number;
|
|
88
|
+
};
|
|
89
|
+
events: {
|
|
90
|
+
kind: "ProjectUpdated";
|
|
91
|
+
before: {
|
|
92
|
+
id: string;
|
|
93
|
+
slug: string;
|
|
94
|
+
name: string;
|
|
95
|
+
status: string;
|
|
96
|
+
description: string;
|
|
97
|
+
createdAt: number;
|
|
98
|
+
updatedAt: number;
|
|
99
|
+
};
|
|
100
|
+
after: {
|
|
101
|
+
id: string;
|
|
102
|
+
slug: string;
|
|
103
|
+
name: string;
|
|
104
|
+
status: string;
|
|
105
|
+
description: string;
|
|
106
|
+
createdAt: number;
|
|
107
|
+
updatedAt: number;
|
|
108
|
+
};
|
|
109
|
+
}[];
|
|
110
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
111
|
+
show: (actor: import("@house-rules/domain").Actor, ref: import("./project/project.js").ProjectRef) => Effect.Effect<{
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly slug: string;
|
|
114
|
+
readonly name: string;
|
|
115
|
+
readonly status: string;
|
|
116
|
+
readonly description: string;
|
|
117
|
+
readonly createdAt: number;
|
|
118
|
+
readonly updatedAt: number;
|
|
119
|
+
}, import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
120
|
+
list: (actor: {
|
|
121
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
122
|
+
}, filter?: {
|
|
123
|
+
readonly status?: string;
|
|
124
|
+
} | undefined) => Effect.Effect<readonly {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly slug: string;
|
|
127
|
+
readonly name: string;
|
|
128
|
+
readonly status: string;
|
|
129
|
+
readonly description: string;
|
|
130
|
+
readonly createdAt: number;
|
|
131
|
+
readonly updatedAt: number;
|
|
132
|
+
}[], import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
133
|
+
}> & {
|
|
134
|
+
readonly id: "projects";
|
|
135
|
+
} & {
|
|
136
|
+
readonly make: (options: {
|
|
137
|
+
readonly statuses: readonly string[];
|
|
138
|
+
readonly defaultStatus: string;
|
|
139
|
+
readonly archivedStatus: string;
|
|
140
|
+
readonly slug: {
|
|
141
|
+
readonly pattern: string;
|
|
142
|
+
readonly maxLength: number;
|
|
143
|
+
readonly lowercase: boolean;
|
|
144
|
+
readonly trim: boolean;
|
|
145
|
+
};
|
|
146
|
+
} | undefined) => Effect.Effect<{
|
|
147
|
+
create: (actor: {
|
|
148
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
149
|
+
}, input: {
|
|
150
|
+
readonly slug: string;
|
|
151
|
+
readonly name: string;
|
|
152
|
+
readonly status?: string;
|
|
153
|
+
readonly description?: string;
|
|
154
|
+
}) => Effect.Effect<{
|
|
155
|
+
project: {
|
|
156
|
+
readonly id: string;
|
|
157
|
+
readonly slug: string;
|
|
158
|
+
readonly name: string;
|
|
159
|
+
readonly status: string;
|
|
160
|
+
readonly description: string;
|
|
161
|
+
readonly createdAt: number;
|
|
162
|
+
readonly updatedAt: number;
|
|
163
|
+
};
|
|
164
|
+
events: {
|
|
165
|
+
kind: "ProjectCreated";
|
|
166
|
+
project: {
|
|
167
|
+
id: string;
|
|
168
|
+
slug: string;
|
|
169
|
+
name: string;
|
|
170
|
+
status: string;
|
|
171
|
+
description: string;
|
|
172
|
+
createdAt: number;
|
|
173
|
+
updatedAt: number;
|
|
174
|
+
};
|
|
175
|
+
}[];
|
|
176
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
177
|
+
update: (actor: {
|
|
178
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
179
|
+
}, ref: {
|
|
180
|
+
readonly id: string;
|
|
181
|
+
} | {
|
|
182
|
+
readonly slug: string;
|
|
183
|
+
}, patch: {
|
|
184
|
+
readonly slug?: string;
|
|
185
|
+
readonly name?: string;
|
|
186
|
+
readonly status?: string;
|
|
187
|
+
readonly description?: string;
|
|
188
|
+
}) => Effect.Effect<{
|
|
189
|
+
project: {
|
|
190
|
+
readonly id: string;
|
|
191
|
+
readonly slug: string;
|
|
192
|
+
readonly name: string;
|
|
193
|
+
readonly status: string;
|
|
194
|
+
readonly description: string;
|
|
195
|
+
readonly createdAt: number;
|
|
196
|
+
readonly updatedAt: number;
|
|
197
|
+
};
|
|
198
|
+
events: {
|
|
199
|
+
kind: "ProjectUpdated";
|
|
200
|
+
before: {
|
|
201
|
+
id: string;
|
|
202
|
+
slug: string;
|
|
203
|
+
name: string;
|
|
204
|
+
status: string;
|
|
205
|
+
description: string;
|
|
206
|
+
createdAt: number;
|
|
207
|
+
updatedAt: number;
|
|
208
|
+
};
|
|
209
|
+
after: {
|
|
210
|
+
id: string;
|
|
211
|
+
slug: string;
|
|
212
|
+
name: string;
|
|
213
|
+
status: string;
|
|
214
|
+
description: string;
|
|
215
|
+
createdAt: number;
|
|
216
|
+
updatedAt: number;
|
|
217
|
+
};
|
|
218
|
+
}[];
|
|
219
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
220
|
+
archive: (actor: import("@house-rules/domain").Actor, ref: import("./project/project.js").ProjectRef) => Effect.Effect<{
|
|
221
|
+
project: {
|
|
222
|
+
readonly id: string;
|
|
223
|
+
readonly slug: string;
|
|
224
|
+
readonly name: string;
|
|
225
|
+
readonly status: string;
|
|
226
|
+
readonly description: string;
|
|
227
|
+
readonly createdAt: number;
|
|
228
|
+
readonly updatedAt: number;
|
|
229
|
+
};
|
|
230
|
+
events: {
|
|
231
|
+
kind: "ProjectUpdated";
|
|
232
|
+
before: {
|
|
233
|
+
id: string;
|
|
234
|
+
slug: string;
|
|
235
|
+
name: string;
|
|
236
|
+
status: string;
|
|
237
|
+
description: string;
|
|
238
|
+
createdAt: number;
|
|
239
|
+
updatedAt: number;
|
|
240
|
+
};
|
|
241
|
+
after: {
|
|
242
|
+
id: string;
|
|
243
|
+
slug: string;
|
|
244
|
+
name: string;
|
|
245
|
+
status: string;
|
|
246
|
+
description: string;
|
|
247
|
+
createdAt: number;
|
|
248
|
+
updatedAt: number;
|
|
249
|
+
};
|
|
250
|
+
}[];
|
|
251
|
+
}, import("@house-rules/domain").NoOpenUnit | import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectSlugTaken | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
252
|
+
show: (actor: import("@house-rules/domain").Actor, ref: import("./project/project.js").ProjectRef) => Effect.Effect<{
|
|
253
|
+
readonly id: string;
|
|
254
|
+
readonly slug: string;
|
|
255
|
+
readonly name: string;
|
|
256
|
+
readonly status: string;
|
|
257
|
+
readonly description: string;
|
|
258
|
+
readonly createdAt: number;
|
|
259
|
+
readonly updatedAt: number;
|
|
260
|
+
}, import("./project/project.js").ProjectAccessDenied | import("./project/project.js").ProjectAccessUnavailable | import("./project/project.js").ProjectNotFound | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
261
|
+
list: (actor: {
|
|
262
|
+
readonly id: string & import("effect/Brand").Brand<"ActorId">;
|
|
263
|
+
}, filter?: {
|
|
264
|
+
readonly status?: string;
|
|
265
|
+
} | undefined) => Effect.Effect<readonly {
|
|
266
|
+
readonly id: string;
|
|
267
|
+
readonly slug: string;
|
|
268
|
+
readonly name: string;
|
|
269
|
+
readonly status: string;
|
|
270
|
+
readonly description: string;
|
|
271
|
+
readonly createdAt: number;
|
|
272
|
+
readonly updatedAt: number;
|
|
273
|
+
}[], import("./project/project.js").ProjectAccessUnavailable | ProjectInputInvalid | import("./project/project.js").ProjectStoreUnavailable, never>;
|
|
274
|
+
}, ProjectInputInvalid, ProjectStore>;
|
|
275
|
+
readonly layer: (options: {
|
|
276
|
+
readonly statuses: readonly string[];
|
|
277
|
+
readonly defaultStatus: string;
|
|
278
|
+
readonly archivedStatus: string;
|
|
279
|
+
readonly slug: {
|
|
280
|
+
readonly pattern: string;
|
|
281
|
+
readonly maxLength: number;
|
|
282
|
+
readonly lowercase: boolean;
|
|
283
|
+
readonly trim: boolean;
|
|
284
|
+
};
|
|
285
|
+
} | undefined) => import("@house-rules/domain").ModuleLayer<Projects, ProjectInputInvalid, ProjectStore>;
|
|
286
|
+
};
|
|
287
|
+
export declare class Projects extends Projects_base {
|
|
288
|
+
static readonly presets: {
|
|
289
|
+
readonly default: {
|
|
290
|
+
readonly statuses: readonly ["active", "archived"];
|
|
291
|
+
readonly defaultStatus: "active";
|
|
292
|
+
readonly archivedStatus: "archived";
|
|
293
|
+
readonly slug: {
|
|
294
|
+
readonly pattern: "^[a-z0-9][a-z0-9-]*$";
|
|
295
|
+
readonly maxLength: 64;
|
|
296
|
+
readonly lowercase: true;
|
|
297
|
+
readonly trim: true;
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
static readonly layerMemory: (options?: ProjectsOptions) => Layer.Layer<Projects, ProjectInputInvalid, never>;
|
|
302
|
+
static readonly layerPostgres: (options?: ProjectsOptions) => Layer.Layer<Projects, ProjectInputInvalid, import("effect/sql/SqlClient").SqlClient>;
|
|
303
|
+
}
|
|
304
|
+
export {};
|
package/dist/facade.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineModule } from "@house-rules/domain";
|
|
2
|
+
import { Effect, Layer, Option, Schema } from "effect";
|
|
3
|
+
import { memoryProjectStore } from "./adapters/memory/project.js";
|
|
4
|
+
import { postgresProjectStore } from "./adapters/postgres/project.js";
|
|
5
|
+
import { ProjectStore } from "./storage.js";
|
|
6
|
+
import { ProjectAccess } from "./project/access.js";
|
|
7
|
+
import { ProjectsOptions, projectsPresets } from "./project/options.js";
|
|
8
|
+
import { ProjectInputInvalid } from "./project/project.js";
|
|
9
|
+
import { makeProjects } from "./project/projects.js";
|
|
10
|
+
export class Projects extends defineModule()("projects", {
|
|
11
|
+
make: (options = projectsPresets.default) => Effect.gen(function* () {
|
|
12
|
+
const invalid = () => new ProjectInputInvalid({ field: "options", message: "Invalid projects options." });
|
|
13
|
+
const settings = yield* Schema.decodeEffect(ProjectsOptions)(options).pipe(Effect.mapError(invalid));
|
|
14
|
+
if (new Set(settings.statuses).size !== settings.statuses.length ||
|
|
15
|
+
!settings.statuses.includes(settings.defaultStatus) ||
|
|
16
|
+
!settings.statuses.includes(settings.archivedStatus))
|
|
17
|
+
return yield* invalid();
|
|
18
|
+
const pattern = yield* Effect.try({
|
|
19
|
+
try: () => new RegExp(settings.slug.pattern),
|
|
20
|
+
catch: invalid,
|
|
21
|
+
});
|
|
22
|
+
return makeProjects(yield* ProjectStore, Option.getOrUndefined(yield* Effect.serviceOption(ProjectAccess)), settings, pattern);
|
|
23
|
+
}),
|
|
24
|
+
}) {
|
|
25
|
+
static presets = projectsPresets;
|
|
26
|
+
static layerMemory = (options = projectsPresets.default) => this.layer(options).pipe(Layer.provide(memoryProjectStore()));
|
|
27
|
+
static layerPostgres = (options = projectsPresets.default) => this.layer(options).pipe(Layer.provide(postgresProjectStore));
|
|
28
|
+
}
|