@marianmeres/ownsuite 1.0.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 +292 -0
- package/API.md +423 -0
- package/CLAUDE.md +3 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/adapters/mock.d.ts +34 -0
- package/dist/adapters/mock.js +73 -0
- package/dist/adapters/mod.d.ts +1 -0
- package/dist/adapters/mod.js +1 -0
- package/dist/domains/base.d.ts +64 -0
- package/dist/domains/base.js +151 -0
- package/dist/domains/mod.d.ts +2 -0
- package/dist/domains/mod.js +2 -0
- package/dist/domains/owned-collection.d.ts +43 -0
- package/dist/domains/owned-collection.js +201 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +31 -0
- package/dist/ownsuite.d.ts +85 -0
- package/dist/ownsuite.js +120 -0
- package/dist/types/adapter.d.ts +49 -0
- package/dist/types/adapter.js +11 -0
- package/dist/types/events.d.ts +63 -0
- package/dist/types/events.js +6 -0
- package/dist/types/mod.d.ts +3 -0
- package/dist/types/mod.js +3 -0
- package/dist/types/state.d.ts +55 -0
- package/dist/types/state.js +8 -0
- package/package.json +29 -0
package/dist/ownsuite.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ownsuite
|
|
3
|
+
*
|
|
4
|
+
* Ownsuite — orchestrator for owner-scoped domain managers.
|
|
5
|
+
*
|
|
6
|
+
* Unlike ecsuite (which hard-codes six e-commerce domains), ownsuite is
|
|
7
|
+
* generic: consumers register arbitrary owner-scoped collection domains by
|
|
8
|
+
* name. Each domain operates through an adapter that talks to a server
|
|
9
|
+
* mount (conventionally `/me/<collection-path>`) with owner scoping
|
|
10
|
+
* enforced by the server via `@marianmeres/collection`'s `ownerIdScope`
|
|
11
|
+
* hook and `@marianmeres/stack-common`'s `ownsuiteOptions()` helper.
|
|
12
|
+
*/
|
|
13
|
+
import { createClog } from "@marianmeres/clog";
|
|
14
|
+
import { createPubSub, } from "@marianmeres/pubsub";
|
|
15
|
+
import { OwnedCollectionManager } from "./domains/owned-collection.js";
|
|
16
|
+
/**
|
|
17
|
+
* Main Ownsuite class — coordinates owner-scoped domain managers.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const suite = createOwnsuite({
|
|
22
|
+
* context: { subjectId: "user-123" },
|
|
23
|
+
* domains: {
|
|
24
|
+
* orders: { adapter: myOrdersAdapter },
|
|
25
|
+
* addresses: { adapter: myAddressesAdapter },
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
* await suite.initialize(); // or pass autoInitialize: true
|
|
29
|
+
*
|
|
30
|
+
* suite.domain("orders").subscribe((s) => console.log(s.state, s.data?.rows));
|
|
31
|
+
* await suite.domain("orders").create({ data: { ... } });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class Ownsuite {
|
|
35
|
+
#clog = createClog("ownsuite", { color: "auto" });
|
|
36
|
+
#pubsub;
|
|
37
|
+
#context;
|
|
38
|
+
// deno-lint-ignore no-explicit-any
|
|
39
|
+
#domains = new Map();
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
this.#pubsub = createPubSub();
|
|
42
|
+
this.#context = { ...(config.context ?? {}) };
|
|
43
|
+
for (const [name, cfg] of Object.entries(config.domains ?? {})) {
|
|
44
|
+
this.registerDomain(name, cfg);
|
|
45
|
+
}
|
|
46
|
+
if (config.autoInitialize) {
|
|
47
|
+
// fire-and-forget; consumers who care should await initialize() explicitly
|
|
48
|
+
this.initialize().catch((e) => this.#clog.error("autoInitialize", e));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Register a new domain after construction. */
|
|
52
|
+
// deno-lint-ignore no-explicit-any
|
|
53
|
+
registerDomain(name, cfg) {
|
|
54
|
+
if (this.#domains.has(name)) {
|
|
55
|
+
throw new Error(`Ownsuite: domain "${name}" already registered`);
|
|
56
|
+
}
|
|
57
|
+
const manager = new OwnedCollectionManager(name, {
|
|
58
|
+
adapter: cfg.adapter,
|
|
59
|
+
getRowId: cfg.getRowId,
|
|
60
|
+
context: this.#context,
|
|
61
|
+
pubsub: this.#pubsub,
|
|
62
|
+
});
|
|
63
|
+
this.#domains.set(name, manager);
|
|
64
|
+
return manager;
|
|
65
|
+
}
|
|
66
|
+
/** Look up a domain manager by name. Throws if unknown. */
|
|
67
|
+
// deno-lint-ignore no-explicit-any
|
|
68
|
+
domain(name) {
|
|
69
|
+
const m = this.#domains.get(name);
|
|
70
|
+
if (!m)
|
|
71
|
+
throw new Error(`Ownsuite: unknown domain "${name}"`);
|
|
72
|
+
return m;
|
|
73
|
+
}
|
|
74
|
+
/** True if a domain by this name is registered. */
|
|
75
|
+
hasDomain(name) {
|
|
76
|
+
return this.#domains.has(name);
|
|
77
|
+
}
|
|
78
|
+
/** List registered domain names. */
|
|
79
|
+
domainNames() {
|
|
80
|
+
return [...this.#domains.keys()];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initialize all registered domains (or a subset). Runs in parallel.
|
|
84
|
+
* Individual domain errors land in that domain's error state — they
|
|
85
|
+
* do not reject the overall promise.
|
|
86
|
+
*/
|
|
87
|
+
async initialize(names) {
|
|
88
|
+
const targets = names ?? this.domainNames();
|
|
89
|
+
await Promise.all(targets.map((n) => this.#domains.get(n)?.initialize() ?? Promise.resolve()));
|
|
90
|
+
}
|
|
91
|
+
/** Update shared context and propagate to all domain managers. */
|
|
92
|
+
setContext(ctx) {
|
|
93
|
+
this.#context = { ...this.#context, ...ctx };
|
|
94
|
+
for (const m of this.#domains.values())
|
|
95
|
+
m.setContext(this.#context);
|
|
96
|
+
}
|
|
97
|
+
getContext() {
|
|
98
|
+
return { ...this.#context };
|
|
99
|
+
}
|
|
100
|
+
/** Subscribe to a specific event type. */
|
|
101
|
+
on(type, subscriber) {
|
|
102
|
+
return this.#pubsub.subscribe(type, subscriber);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Subscribe to all events. Wildcard subscribers receive an envelope
|
|
106
|
+
* `{ event: string, data: OwnsuiteEvent }` — see `@marianmeres/pubsub`.
|
|
107
|
+
*/
|
|
108
|
+
onAny(subscriber) {
|
|
109
|
+
return this.#pubsub.subscribe("*", subscriber);
|
|
110
|
+
}
|
|
111
|
+
/** Reset all domains to initializing state. */
|
|
112
|
+
reset() {
|
|
113
|
+
for (const m of this.#domains.values())
|
|
114
|
+
m.reset();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Convenience factory matching the ecsuite `createECSuite` convention. */
|
|
118
|
+
export function createOwnsuite(config = {}) {
|
|
119
|
+
return new Ownsuite(config);
|
|
120
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types/adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter interface for server communication. One adapter instance drives
|
|
5
|
+
* one owner-scoped domain (a single collection path on the server).
|
|
6
|
+
*
|
|
7
|
+
* Implementations typically wrap `fetch()` calls against the stack's
|
|
8
|
+
* `/me/<collection-path>/mod` endpoints; see `stack-common/ownsuite` for
|
|
9
|
+
* the matching server-side convention.
|
|
10
|
+
*/
|
|
11
|
+
import type { OwnsuiteContext } from "./state.js";
|
|
12
|
+
/**
|
|
13
|
+
* Standard list result shape: rows + meta. Matches the collection package's
|
|
14
|
+
* REST response envelope for consistency, but adapters are free to return
|
|
15
|
+
* whatever shape their server uses and map it here.
|
|
16
|
+
*/
|
|
17
|
+
export interface OwnedListResult<TRow> {
|
|
18
|
+
data: TRow[];
|
|
19
|
+
meta: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Standard single-row result shape: one row + meta. Also matches the
|
|
23
|
+
* collection package's REST envelope.
|
|
24
|
+
*/
|
|
25
|
+
export interface OwnedRowResult<TRow> {
|
|
26
|
+
data: TRow;
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Adapter for a single owner-scoped collection domain.
|
|
31
|
+
*
|
|
32
|
+
* Notes for implementors:
|
|
33
|
+
* - `owner_id` is enforced server-side. Do NOT attempt to pass it from the
|
|
34
|
+
* client. The client can only act on rows it owns.
|
|
35
|
+
* - Errors should throw (ideally `HTTP_ERROR` from `@marianmeres/http-utils`);
|
|
36
|
+
* the manager handles rollback and error state.
|
|
37
|
+
*/
|
|
38
|
+
export interface OwnedCollectionAdapter<TRow, TCreate = unknown, TUpdate = unknown> {
|
|
39
|
+
/** List rows owned by the current subject. Query params are implementation-defined. */
|
|
40
|
+
list(ctx: OwnsuiteContext, query?: Record<string, unknown>): Promise<OwnedListResult<TRow>>;
|
|
41
|
+
/** Get one row by id (server returns 404 if not owned by subject). */
|
|
42
|
+
getOne(id: string, ctx: OwnsuiteContext): Promise<OwnedRowResult<TRow>>;
|
|
43
|
+
/** Create a new row (server stamps owner_id from the authenticated subject). */
|
|
44
|
+
create(data: TCreate, ctx: OwnsuiteContext): Promise<OwnedRowResult<TRow>>;
|
|
45
|
+
/** Update a row by id (owner_id is immutable server-side). */
|
|
46
|
+
update(id: string, data: TUpdate, ctx: OwnsuiteContext): Promise<OwnedRowResult<TRow>>;
|
|
47
|
+
/** Delete a row by id (404 if not owned). */
|
|
48
|
+
delete(id: string, ctx: OwnsuiteContext): Promise<boolean>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types/adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter interface for server communication. One adapter instance drives
|
|
5
|
+
* one owner-scoped domain (a single collection path on the server).
|
|
6
|
+
*
|
|
7
|
+
* Implementations typically wrap `fetch()` calls against the stack's
|
|
8
|
+
* `/me/<collection-path>/mod` endpoints; see `stack-common/ownsuite` for
|
|
9
|
+
* the matching server-side convention.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types/events
|
|
3
|
+
*
|
|
4
|
+
* Event type definitions for the ownsuite event system.
|
|
5
|
+
*/
|
|
6
|
+
import type { DomainError, DomainState } from "./state.js";
|
|
7
|
+
/**
|
|
8
|
+
* Domain identifier in ownsuite is an arbitrary string (the collection name
|
|
9
|
+
* or any label the consumer chose), unlike ecsuite's fixed enum of six
|
|
10
|
+
* domains. Users register their own domains by name.
|
|
11
|
+
*/
|
|
12
|
+
export type DomainName = string;
|
|
13
|
+
/** Event types emitted by the suite. */
|
|
14
|
+
export type OwnsuiteEventType = "domain:state:changed" | "domain:error" | "domain:synced" | "own:list:fetched" | "own:row:fetched" | "own:row:created" | "own:row:updated" | "own:row:deleted";
|
|
15
|
+
/** Base event data. */
|
|
16
|
+
export interface OwnsuiteEventBase {
|
|
17
|
+
/** Event timestamp */
|
|
18
|
+
timestamp: number;
|
|
19
|
+
/** Domain that emitted the event */
|
|
20
|
+
domain: DomainName;
|
|
21
|
+
}
|
|
22
|
+
/** State change event. */
|
|
23
|
+
export interface StateChangedEvent extends OwnsuiteEventBase {
|
|
24
|
+
type: "domain:state:changed";
|
|
25
|
+
previousState: DomainState;
|
|
26
|
+
newState: DomainState;
|
|
27
|
+
}
|
|
28
|
+
/** Error event. */
|
|
29
|
+
export interface ErrorEvent extends OwnsuiteEventBase {
|
|
30
|
+
type: "domain:error";
|
|
31
|
+
error: DomainError;
|
|
32
|
+
}
|
|
33
|
+
/** Sync completed event. */
|
|
34
|
+
export interface SyncedEvent extends OwnsuiteEventBase {
|
|
35
|
+
type: "domain:synced";
|
|
36
|
+
}
|
|
37
|
+
/** List fetched event. */
|
|
38
|
+
export interface ListFetchedEvent extends OwnsuiteEventBase {
|
|
39
|
+
type: "own:list:fetched";
|
|
40
|
+
count: number;
|
|
41
|
+
}
|
|
42
|
+
/** Single row fetched event. */
|
|
43
|
+
export interface RowFetchedEvent extends OwnsuiteEventBase {
|
|
44
|
+
type: "own:row:fetched";
|
|
45
|
+
rowId: string;
|
|
46
|
+
}
|
|
47
|
+
/** Row created event. */
|
|
48
|
+
export interface RowCreatedEvent extends OwnsuiteEventBase {
|
|
49
|
+
type: "own:row:created";
|
|
50
|
+
rowId: string;
|
|
51
|
+
}
|
|
52
|
+
/** Row updated event. */
|
|
53
|
+
export interface RowUpdatedEvent extends OwnsuiteEventBase {
|
|
54
|
+
type: "own:row:updated";
|
|
55
|
+
rowId: string;
|
|
56
|
+
}
|
|
57
|
+
/** Row deleted event. */
|
|
58
|
+
export interface RowDeletedEvent extends OwnsuiteEventBase {
|
|
59
|
+
type: "own:row:deleted";
|
|
60
|
+
rowId: string;
|
|
61
|
+
}
|
|
62
|
+
/** All event types union. */
|
|
63
|
+
export type OwnsuiteEvent = StateChangedEvent | ErrorEvent | SyncedEvent | ListFetchedEvent | RowFetchedEvent | RowCreatedEvent | RowUpdatedEvent | RowDeletedEvent;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module types/state
|
|
3
|
+
*
|
|
4
|
+
* Core state and context types for ownsuite domain managers.
|
|
5
|
+
* Mirrors the shape of `@marianmeres/ecsuite` but specialized for
|
|
6
|
+
* owner-scoped collection CRUD (a single list of rows per domain).
|
|
7
|
+
*/
|
|
8
|
+
/** Domain state progression — identical to ecsuite for ecosystem consistency. */
|
|
9
|
+
export type DomainState = "initializing" | "ready" | "syncing" | "error";
|
|
10
|
+
/** Error information structure. */
|
|
11
|
+
export interface DomainError {
|
|
12
|
+
/** Error code for programmatic handling */
|
|
13
|
+
code: string;
|
|
14
|
+
/** Human-readable message */
|
|
15
|
+
message: string;
|
|
16
|
+
/** Operation that failed */
|
|
17
|
+
operation: string;
|
|
18
|
+
/** Original error for debugging */
|
|
19
|
+
originalError?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** Base state wrapper for all domains. */
|
|
22
|
+
export interface DomainStateWrapper<T> {
|
|
23
|
+
/** Current domain state */
|
|
24
|
+
state: DomainState;
|
|
25
|
+
/** Domain data (null during initialization or after a critical error) */
|
|
26
|
+
data: T | null;
|
|
27
|
+
/** Error information when state is "error" */
|
|
28
|
+
error: DomainError | null;
|
|
29
|
+
/** Timestamp of last successful sync */
|
|
30
|
+
lastSyncedAt: number | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Context passed to adapters. Ownsuite does not require the caller to know
|
|
34
|
+
* its own `ownerId` — the server resolves it from the authenticated subject
|
|
35
|
+
* via the `/me/*` mount. The context is still provided so adapters can pass
|
|
36
|
+
* arbitrary host-app data (correlation ids, feature flags, etc.) through.
|
|
37
|
+
*/
|
|
38
|
+
export interface OwnsuiteContext {
|
|
39
|
+
/** Hint — not used for authorization. The server is authoritative. */
|
|
40
|
+
subjectId?: string;
|
|
41
|
+
/** Additional context properties for adapter-specific needs. */
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* State shape for a single owner-scoped collection domain: a list of rows
|
|
46
|
+
* plus pagination/meta info from the last list operation. Individual-row
|
|
47
|
+
* operations (get/update/delete) mutate the list in place without replacing
|
|
48
|
+
* meta, so optimistic updates work naturally.
|
|
49
|
+
*/
|
|
50
|
+
export interface OwnedCollectionState<TRow> {
|
|
51
|
+
/** Rows owned by the current subject (server-scoped). */
|
|
52
|
+
rows: TRow[];
|
|
53
|
+
/** Metadata from the last list response (total count, pagination, etc.). */
|
|
54
|
+
meta: Record<string, unknown>;
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marianmeres/ownsuite",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/mod.js",
|
|
6
|
+
"types": "dist/mod.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/mod.d.ts",
|
|
10
|
+
"import": "./dist/mod.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"author": "Marian Meres",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@marianmeres/clog": "^3.15.2",
|
|
17
|
+
"@marianmeres/collection-types": "^1.36.0",
|
|
18
|
+
"@marianmeres/http-utils": "^2.5.1",
|
|
19
|
+
"@marianmeres/pubsub": "^2.4.6",
|
|
20
|
+
"@marianmeres/store": "^2.4.4"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/marianmeres/ownsuite.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/marianmeres/ownsuite/issues"
|
|
28
|
+
}
|
|
29
|
+
}
|