@directive-run/knowledge 0.2.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 +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Example: pagination
|
|
2
|
+
// Source: examples/pagination/src/pagination.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pagination & Infinite Scroll — Directive Modules
|
|
7
|
+
*
|
|
8
|
+
* Two modules: `filters` owns search/sort/category,
|
|
9
|
+
* `list` owns items and pagination state with crossModuleDeps.
|
|
10
|
+
*
|
|
11
|
+
* Constraints:
|
|
12
|
+
* - loadMore: appends next page when scrollNearBottom
|
|
13
|
+
* - filterChanged: resets and re-fetches when filters change
|
|
14
|
+
*
|
|
15
|
+
* Effects:
|
|
16
|
+
* - observeScroll: IntersectionObserver on sentinel element
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type ModuleSchema,
|
|
21
|
+
createModule,
|
|
22
|
+
createSystem,
|
|
23
|
+
t,
|
|
24
|
+
} from "@directive-run/core";
|
|
25
|
+
import { devtoolsPlugin, loggingPlugin } from "@directive-run/core/plugins";
|
|
26
|
+
import { type ListItem, fetchPage } from "./mock-api.js";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Filters Module
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export const filtersSchema = {
|
|
33
|
+
facts: {
|
|
34
|
+
search: t.string(),
|
|
35
|
+
sortBy: t.string<"newest" | "oldest" | "title">(),
|
|
36
|
+
category: t.string(),
|
|
37
|
+
},
|
|
38
|
+
events: {
|
|
39
|
+
setSearch: { value: t.string() },
|
|
40
|
+
setSortBy: { value: t.string() },
|
|
41
|
+
setCategory: { value: t.string() },
|
|
42
|
+
},
|
|
43
|
+
} satisfies ModuleSchema;
|
|
44
|
+
|
|
45
|
+
export const filtersModule = createModule("filters", {
|
|
46
|
+
schema: filtersSchema,
|
|
47
|
+
|
|
48
|
+
init: (facts) => {
|
|
49
|
+
facts.search = "";
|
|
50
|
+
facts.sortBy = "newest";
|
|
51
|
+
facts.category = "all";
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
events: {
|
|
55
|
+
setSearch: (facts, { value }) => {
|
|
56
|
+
facts.search = value;
|
|
57
|
+
},
|
|
58
|
+
setSortBy: (facts, { value }) => {
|
|
59
|
+
facts.sortBy = value;
|
|
60
|
+
},
|
|
61
|
+
setCategory: (facts, { value }) => {
|
|
62
|
+
facts.category = value;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// List Module
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export const listSchema = {
|
|
72
|
+
facts: {
|
|
73
|
+
items: t.object<ListItem[]>(),
|
|
74
|
+
cursor: t.string(),
|
|
75
|
+
hasMore: t.boolean(),
|
|
76
|
+
isLoadingMore: t.boolean(),
|
|
77
|
+
scrollNearBottom: t.boolean(),
|
|
78
|
+
lastFilterHash: t.string(),
|
|
79
|
+
},
|
|
80
|
+
derivations: {
|
|
81
|
+
totalLoaded: t.number(),
|
|
82
|
+
isEmpty: t.boolean(),
|
|
83
|
+
},
|
|
84
|
+
events: {
|
|
85
|
+
setScrollNearBottom: { value: t.boolean() },
|
|
86
|
+
},
|
|
87
|
+
requirements: {
|
|
88
|
+
LOAD_PAGE: {
|
|
89
|
+
cursor: t.string(),
|
|
90
|
+
search: t.string(),
|
|
91
|
+
sortBy: t.string(),
|
|
92
|
+
category: t.string(),
|
|
93
|
+
},
|
|
94
|
+
RESET_AND_LOAD: {
|
|
95
|
+
search: t.string(),
|
|
96
|
+
sortBy: t.string(),
|
|
97
|
+
category: t.string(),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
} satisfies ModuleSchema;
|
|
101
|
+
|
|
102
|
+
export const listModule = createModule("list", {
|
|
103
|
+
schema: listSchema,
|
|
104
|
+
|
|
105
|
+
crossModuleDeps: { filters: filtersSchema },
|
|
106
|
+
|
|
107
|
+
init: (facts) => {
|
|
108
|
+
facts.items = [];
|
|
109
|
+
facts.cursor = "";
|
|
110
|
+
facts.hasMore = true;
|
|
111
|
+
facts.isLoadingMore = false;
|
|
112
|
+
facts.scrollNearBottom = false;
|
|
113
|
+
facts.lastFilterHash = "";
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Derivations
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
derive: {
|
|
121
|
+
totalLoaded: (facts) => facts.self.items.length,
|
|
122
|
+
isEmpty: (facts) => facts.self.items.length === 0 && !facts.self.hasMore,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Events
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
events: {
|
|
130
|
+
setScrollNearBottom: (facts, { value }) => {
|
|
131
|
+
facts.scrollNearBottom = value;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Constraints
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
constraints: {
|
|
140
|
+
loadMore: {
|
|
141
|
+
when: (facts) => {
|
|
142
|
+
return (
|
|
143
|
+
facts.self.hasMore &&
|
|
144
|
+
!facts.self.isLoadingMore &&
|
|
145
|
+
facts.self.scrollNearBottom
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
require: (facts) => ({
|
|
149
|
+
type: "LOAD_PAGE",
|
|
150
|
+
cursor: facts.self.cursor,
|
|
151
|
+
search: facts.filters.search,
|
|
152
|
+
sortBy: facts.filters.sortBy,
|
|
153
|
+
category: facts.filters.category,
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
filterChanged: {
|
|
158
|
+
when: (facts) => {
|
|
159
|
+
const hash = `${facts.filters.search}|${facts.filters.sortBy}|${facts.filters.category}`;
|
|
160
|
+
|
|
161
|
+
return hash !== facts.self.lastFilterHash;
|
|
162
|
+
},
|
|
163
|
+
require: (facts) => ({
|
|
164
|
+
type: "RESET_AND_LOAD",
|
|
165
|
+
search: facts.filters.search,
|
|
166
|
+
sortBy: facts.filters.sortBy,
|
|
167
|
+
category: facts.filters.category,
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Resolvers
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
resolvers: {
|
|
177
|
+
loadPage: {
|
|
178
|
+
requirement: "LOAD_PAGE",
|
|
179
|
+
resolve: async (req, context) => {
|
|
180
|
+
context.facts.isLoadingMore = true;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const data = await fetchPage(req.cursor, 20, {
|
|
184
|
+
search: req.search,
|
|
185
|
+
sortBy: req.sortBy,
|
|
186
|
+
category: req.category,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
context.facts.items = [...context.facts.items, ...data.items];
|
|
190
|
+
context.facts.cursor = data.nextCursor;
|
|
191
|
+
context.facts.hasMore = data.hasMore;
|
|
192
|
+
} finally {
|
|
193
|
+
context.facts.isLoadingMore = false;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
resetAndLoad: {
|
|
199
|
+
requirement: "RESET_AND_LOAD",
|
|
200
|
+
resolve: async (req, context) => {
|
|
201
|
+
const hash = `${req.search}|${req.sortBy}|${req.category}`;
|
|
202
|
+
|
|
203
|
+
context.facts.items = [];
|
|
204
|
+
context.facts.cursor = "";
|
|
205
|
+
context.facts.hasMore = true;
|
|
206
|
+
context.facts.isLoadingMore = true;
|
|
207
|
+
context.facts.lastFilterHash = hash;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const data = await fetchPage("", 20, {
|
|
211
|
+
search: req.search,
|
|
212
|
+
sortBy: req.sortBy,
|
|
213
|
+
category: req.category,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
context.facts.items = data.items;
|
|
217
|
+
context.facts.cursor = data.nextCursor;
|
|
218
|
+
context.facts.hasMore = data.hasMore;
|
|
219
|
+
} finally {
|
|
220
|
+
context.facts.isLoadingMore = false;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Effects
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
effects: {
|
|
231
|
+
observeScroll: {
|
|
232
|
+
run: (facts) => {
|
|
233
|
+
const sentinel = document.getElementById("pg-scroll-sentinel");
|
|
234
|
+
if (!sentinel) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const observer = new IntersectionObserver(
|
|
239
|
+
([entry]) => {
|
|
240
|
+
facts.self.scrollNearBottom = entry.isIntersecting;
|
|
241
|
+
},
|
|
242
|
+
{ rootMargin: "200px" },
|
|
243
|
+
);
|
|
244
|
+
observer.observe(sentinel);
|
|
245
|
+
|
|
246
|
+
return () => observer.disconnect();
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// System
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
export const system = createSystem({
|
|
257
|
+
modules: { filters: filtersModule, list: listModule },
|
|
258
|
+
debug: { runHistory: true },
|
|
259
|
+
plugins: [loggingPlugin(), devtoolsPlugin({ name: "pagination" })],
|
|
260
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// Example: permissions
|
|
2
|
+
// Source: examples/permissions/src/permissions.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Role-Based Permissions — Directive Modules
|
|
7
|
+
*
|
|
8
|
+
* Three modules demonstrate cross-module constraint resolution:
|
|
9
|
+
* - auth: manages login state (role, userName, token)
|
|
10
|
+
* - permissions: loads permissions based on auth role, derives capability flags
|
|
11
|
+
* - content: manages articles with permission-gated actions
|
|
12
|
+
*
|
|
13
|
+
* The system uses `crossModuleDeps` so constraints in one module
|
|
14
|
+
* can react to derivations/facts from another module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type ModuleSchema,
|
|
19
|
+
createModule,
|
|
20
|
+
createSystem,
|
|
21
|
+
t,
|
|
22
|
+
} from "@directive-run/core";
|
|
23
|
+
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
24
|
+
import {
|
|
25
|
+
type Article,
|
|
26
|
+
deleteArticle as apiDeleteArticle,
|
|
27
|
+
fetchArticles as apiFetchArticles,
|
|
28
|
+
fetchPermissions as apiFetchPermissions,
|
|
29
|
+
publishArticle as apiPublishArticle,
|
|
30
|
+
} from "./mock-api.js";
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Preset Users
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const presetUsers: Record<
|
|
37
|
+
string,
|
|
38
|
+
{ userName: string; role: string; token: string }
|
|
39
|
+
> = {
|
|
40
|
+
alice: { userName: "Alice", role: "admin", token: "tok-alice-admin" },
|
|
41
|
+
bob: { userName: "Bob", role: "editor", token: "tok-bob-editor" },
|
|
42
|
+
carol: { userName: "Carol", role: "viewer", token: "tok-carol-viewer" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Auth Module
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export const authSchema = {
|
|
50
|
+
facts: {
|
|
51
|
+
role: t.string(),
|
|
52
|
+
userName: t.string(),
|
|
53
|
+
token: t.string(),
|
|
54
|
+
},
|
|
55
|
+
derivations: {
|
|
56
|
+
isAuthenticated: t.boolean(),
|
|
57
|
+
},
|
|
58
|
+
events: {
|
|
59
|
+
login: { userId: t.string() },
|
|
60
|
+
logout: {},
|
|
61
|
+
},
|
|
62
|
+
requirements: {},
|
|
63
|
+
} satisfies ModuleSchema;
|
|
64
|
+
|
|
65
|
+
export const authModule = createModule("auth", {
|
|
66
|
+
schema: authSchema,
|
|
67
|
+
|
|
68
|
+
init: (facts) => {
|
|
69
|
+
facts.role = "";
|
|
70
|
+
facts.userName = "";
|
|
71
|
+
facts.token = "";
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
derive: {
|
|
75
|
+
isAuthenticated: (facts) => facts.token !== "",
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
events: {
|
|
79
|
+
login: (facts, { userId }) => {
|
|
80
|
+
const preset = presetUsers[userId];
|
|
81
|
+
if (!preset) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
facts.token = preset.token;
|
|
86
|
+
facts.userName = preset.userName;
|
|
87
|
+
facts.role = preset.role;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
logout: (facts) => {
|
|
91
|
+
facts.token = "";
|
|
92
|
+
facts.userName = "";
|
|
93
|
+
facts.role = "";
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Permissions Module
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
export const permissionsSchema = {
|
|
103
|
+
facts: {
|
|
104
|
+
permissions: t.object<string[]>(),
|
|
105
|
+
loaded: t.boolean(),
|
|
106
|
+
},
|
|
107
|
+
derivations: {
|
|
108
|
+
canEdit: t.boolean(),
|
|
109
|
+
canPublish: t.boolean(),
|
|
110
|
+
canDelete: t.boolean(),
|
|
111
|
+
canManageUsers: t.boolean(),
|
|
112
|
+
canViewAnalytics: t.boolean(),
|
|
113
|
+
isAdmin: t.boolean(),
|
|
114
|
+
permissionCount: t.number(),
|
|
115
|
+
},
|
|
116
|
+
events: {
|
|
117
|
+
reset: {},
|
|
118
|
+
},
|
|
119
|
+
requirements: {
|
|
120
|
+
FETCH_PERMISSIONS: { role: t.string() },
|
|
121
|
+
},
|
|
122
|
+
} satisfies ModuleSchema;
|
|
123
|
+
|
|
124
|
+
export const permissionsModule = createModule("permissions", {
|
|
125
|
+
schema: permissionsSchema,
|
|
126
|
+
|
|
127
|
+
crossModuleDeps: { auth: authSchema },
|
|
128
|
+
|
|
129
|
+
init: (facts) => {
|
|
130
|
+
facts.permissions = [];
|
|
131
|
+
facts.loaded = false;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
derive: {
|
|
135
|
+
canEdit: (facts) =>
|
|
136
|
+
(facts.self.permissions as string[]).includes("content.edit"),
|
|
137
|
+
canPublish: (facts) =>
|
|
138
|
+
(facts.self.permissions as string[]).includes("content.publish"),
|
|
139
|
+
canDelete: (facts) =>
|
|
140
|
+
(facts.self.permissions as string[]).includes("content.delete"),
|
|
141
|
+
canManageUsers: (facts) =>
|
|
142
|
+
(facts.self.permissions as string[]).includes("users.manage"),
|
|
143
|
+
canViewAnalytics: (facts) =>
|
|
144
|
+
(facts.self.permissions as string[]).includes("analytics.view"),
|
|
145
|
+
isAdmin: (_facts, derive) => derive.canManageUsers as boolean,
|
|
146
|
+
permissionCount: (facts) => (facts.self.permissions as string[]).length,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
events: {
|
|
150
|
+
reset: (facts) => {
|
|
151
|
+
facts.permissions = [];
|
|
152
|
+
facts.loaded = false;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
constraints: {
|
|
157
|
+
loadPermissions: {
|
|
158
|
+
when: (facts) => {
|
|
159
|
+
return (
|
|
160
|
+
(facts.auth.token as string) !== "" && !(facts.self.loaded as boolean)
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
require: (facts) => ({
|
|
164
|
+
type: "FETCH_PERMISSIONS",
|
|
165
|
+
role: facts.auth.role as string,
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
resolvers: {
|
|
171
|
+
fetchPermissions: {
|
|
172
|
+
requirement: "FETCH_PERMISSIONS",
|
|
173
|
+
timeout: 5000,
|
|
174
|
+
resolve: async (req, context) => {
|
|
175
|
+
const perms = await apiFetchPermissions(req.role);
|
|
176
|
+
context.facts.permissions = perms;
|
|
177
|
+
context.facts.loaded = true;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Content Module
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
export const contentSchema = {
|
|
188
|
+
facts: {
|
|
189
|
+
articles: t.object<Article[]>(),
|
|
190
|
+
loaded: t.boolean(),
|
|
191
|
+
publishRequested: t.string(),
|
|
192
|
+
deleteRequested: t.string(),
|
|
193
|
+
actionStatus: t.string(),
|
|
194
|
+
},
|
|
195
|
+
derivations: {},
|
|
196
|
+
events: {
|
|
197
|
+
requestPublish: { articleId: t.string() },
|
|
198
|
+
requestDelete: { articleId: t.string() },
|
|
199
|
+
clearAction: {},
|
|
200
|
+
},
|
|
201
|
+
requirements: {
|
|
202
|
+
LOAD_CONTENT: {},
|
|
203
|
+
PUBLISH_ARTICLE: { articleId: t.string() },
|
|
204
|
+
DELETE_ARTICLE: { articleId: t.string() },
|
|
205
|
+
},
|
|
206
|
+
} satisfies ModuleSchema;
|
|
207
|
+
|
|
208
|
+
export const contentModule = createModule("content", {
|
|
209
|
+
schema: contentSchema,
|
|
210
|
+
|
|
211
|
+
crossModuleDeps: { auth: authSchema, permissions: permissionsSchema },
|
|
212
|
+
|
|
213
|
+
init: (facts) => {
|
|
214
|
+
facts.articles = [];
|
|
215
|
+
facts.loaded = false;
|
|
216
|
+
facts.publishRequested = "";
|
|
217
|
+
facts.deleteRequested = "";
|
|
218
|
+
facts.actionStatus = "idle";
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
constraints: {
|
|
222
|
+
loadContent: {
|
|
223
|
+
when: (facts) => {
|
|
224
|
+
return (
|
|
225
|
+
(facts.auth.token as string) !== "" && !(facts.self.loaded as boolean)
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
require: { type: "LOAD_CONTENT" },
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
publishArticle: {
|
|
232
|
+
when: (facts) => {
|
|
233
|
+
return (
|
|
234
|
+
(facts.self.publishRequested as string) !== "" &&
|
|
235
|
+
(facts.permissions.permissions as string[]).includes(
|
|
236
|
+
"content.publish",
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
},
|
|
240
|
+
require: (facts) => ({
|
|
241
|
+
type: "PUBLISH_ARTICLE",
|
|
242
|
+
articleId: facts.self.publishRequested as string,
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
deleteArticle: {
|
|
247
|
+
when: (facts) => {
|
|
248
|
+
return (
|
|
249
|
+
(facts.self.deleteRequested as string) !== "" &&
|
|
250
|
+
(facts.permissions.permissions as string[]).includes("content.delete")
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
require: (facts) => ({
|
|
254
|
+
type: "DELETE_ARTICLE",
|
|
255
|
+
articleId: facts.self.deleteRequested as string,
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
resolvers: {
|
|
261
|
+
loadContent: {
|
|
262
|
+
requirement: "LOAD_CONTENT",
|
|
263
|
+
timeout: 5000,
|
|
264
|
+
resolve: async (_req, context) => {
|
|
265
|
+
const articles = await apiFetchArticles();
|
|
266
|
+
context.facts.articles = articles;
|
|
267
|
+
context.facts.loaded = true;
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
publishArticle: {
|
|
272
|
+
requirement: "PUBLISH_ARTICLE",
|
|
273
|
+
timeout: 5000,
|
|
274
|
+
resolve: async (req, context) => {
|
|
275
|
+
context.facts.actionStatus = "publishing";
|
|
276
|
+
await apiPublishArticle(req.articleId);
|
|
277
|
+
|
|
278
|
+
const articles = context.facts.articles as Article[];
|
|
279
|
+
context.facts.articles = articles.map((a) => {
|
|
280
|
+
if (a.id === req.articleId) {
|
|
281
|
+
return { ...a, status: "published" as const };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return a;
|
|
285
|
+
});
|
|
286
|
+
context.facts.publishRequested = "";
|
|
287
|
+
context.facts.actionStatus = "done";
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
deleteArticle: {
|
|
292
|
+
requirement: "DELETE_ARTICLE",
|
|
293
|
+
timeout: 5000,
|
|
294
|
+
resolve: async (req, context) => {
|
|
295
|
+
context.facts.actionStatus = "deleting";
|
|
296
|
+
await apiDeleteArticle(req.articleId);
|
|
297
|
+
|
|
298
|
+
const articles = context.facts.articles as Article[];
|
|
299
|
+
context.facts.articles = articles.filter((a) => a.id !== req.articleId);
|
|
300
|
+
context.facts.deleteRequested = "";
|
|
301
|
+
context.facts.actionStatus = "done";
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
events: {
|
|
307
|
+
requestPublish: (facts, { articleId }) => {
|
|
308
|
+
facts.publishRequested = articleId;
|
|
309
|
+
facts.actionStatus = "idle";
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
requestDelete: (facts, { articleId }) => {
|
|
313
|
+
facts.deleteRequested = articleId;
|
|
314
|
+
facts.actionStatus = "idle";
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
clearAction: (facts) => {
|
|
318
|
+
facts.publishRequested = "";
|
|
319
|
+
facts.deleteRequested = "";
|
|
320
|
+
facts.actionStatus = "idle";
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// System
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
export const system = createSystem({
|
|
330
|
+
modules: {
|
|
331
|
+
auth: authModule,
|
|
332
|
+
permissions: permissionsModule,
|
|
333
|
+
content: contentModule,
|
|
334
|
+
},
|
|
335
|
+
debug: { runHistory: true },
|
|
336
|
+
plugins: [devtoolsPlugin({ name: "permissions" })],
|
|
337
|
+
});
|