create-flowdular 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bin.js +526 -0
  4. package/package.json +35 -0
  5. package/template/default/.prettierrc.json +6 -0
  6. package/template/default/README.md +56 -0
  7. package/template/default/_gitignore +8 -0
  8. package/template/default/flowdular.json +14 -0
  9. package/template/default/modules/example/migrations/0001_example_core.down.sql +1 -0
  10. package/template/default/modules/example/migrations/0001_example_core.up.sql +14 -0
  11. package/template/default/modules/example/module.json +22 -0
  12. package/template/default/modules/example/package.json +27 -0
  13. package/template/default/modules/example/spec/module.yaml +43 -0
  14. package/template/default/modules/example/src/acl/permissions.ts +6 -0
  15. package/template/default/modules/example/src/api/endpoints.ts +81 -0
  16. package/template/default/modules/example/src/client/NotesView.tsrx +236 -0
  17. package/template/default/modules/example/src/client/api.ts +38 -0
  18. package/template/default/modules/example/src/client/contribution.tsrx +52 -0
  19. package/template/default/modules/example/src/client/index.ts +18 -0
  20. package/template/default/modules/example/src/client/navigation-copy.ts +9 -0
  21. package/template/default/modules/example/src/client/state.ts +14 -0
  22. package/template/default/modules/example/src/domain/types.ts +12 -0
  23. package/template/default/modules/example/src/index.ts +24 -0
  24. package/template/default/modules/example/src/platform.ts +23 -0
  25. package/template/default/modules/example/src/server/index.ts +3 -0
  26. package/template/default/modules/example/src/server/runtime.ts +84 -0
  27. package/template/default/modules/example/src/services/database-repository.ts +82 -0
  28. package/template/default/modules/example/src/services/index.ts +6 -0
  29. package/template/default/modules/example/src/services/migration.ts +35 -0
  30. package/template/default/modules/example/src/services/note-service.ts +54 -0
  31. package/template/default/modules/example/src/services/repository.ts +7 -0
  32. package/template/default/modules/example/tests/module.test.ts +67 -0
  33. package/template/default/modules/example/tests/notes.test.ts +83 -0
  34. package/template/default/modules/example/tests/support/database.ts +69 -0
  35. package/template/default/modules/example/translations/en.json +34 -0
  36. package/template/default/modules/example/translations/pl.json +34 -0
  37. package/template/default/modules/example/tsconfig.json +14 -0
  38. package/template/default/modules/example/vitest.config.ts +10 -0
  39. package/template/default/package.json +26 -0
  40. package/template/default/platform/index.html +13 -0
  41. package/template/default/platform/octane.config.ts +117 -0
  42. package/template/default/platform/package.json +27 -0
  43. package/template/default/platform/public/favicon.svg +1 -0
  44. package/template/default/platform/scripts/dev.mjs +18 -0
  45. package/template/default/platform/src/App.tsrx +80 -0
  46. package/template/default/platform/src/generated/modules.client.ts +15 -0
  47. package/template/default/platform/src/generated/modules.server.ts +31 -0
  48. package/template/default/platform/src/server/database.ts +25 -0
  49. package/template/default/platform/src/styles.css +39 -0
  50. package/template/default/platform/tsconfig.json +15 -0
  51. package/template/default/platform/vite.config.ts +16 -0
  52. package/template/default/pnpm-workspace.yaml +6 -0
  53. package/template/default/tsconfig.base.json +18 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "./node_modules/@flowdular/contracts/schemas/project.schema.json",
3
+ "schemaVersion": 1,
4
+ "architectureVersion": "0.2.0",
5
+ "specs": {
6
+ "platformRoot": "specs",
7
+ "moduleDirectory": "spec"
8
+ },
9
+ "modules": {
10
+ "roots": ["modules"],
11
+ "enabled": ["system.core", "auth.core", "example.core"]
12
+ },
13
+ "locales": ["en", "pl"]
14
+ }
@@ -0,0 +1 @@
1
+ DROP TABLE IF EXISTS example_notes;
@@ -0,0 +1,14 @@
1
+ CREATE TABLE IF NOT EXISTS example_notes (
2
+ tenant_id TEXT NOT NULL,
3
+ id TEXT NOT NULL,
4
+ title TEXT NOT NULL,
5
+ body TEXT NOT NULL,
6
+ created_at BIGINT NOT NULL,
7
+ PRIMARY KEY (tenant_id, id)
8
+ );
9
+ CREATE INDEX IF NOT EXISTS example_notes_tenant_created_idx ON example_notes (tenant_id, created_at DESC);
10
+ ALTER TABLE example_notes ENABLE ROW LEVEL SECURITY;
11
+ ALTER TABLE example_notes FORCE ROW LEVEL SECURITY;
12
+ CREATE POLICY example_notes_tenant_policy ON example_notes
13
+ USING (tenant_id = current_setting('coreloom.tenant_id', true))
14
+ WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "../../node_modules/@flowdular/contracts/schemas/module.schema.json",
3
+ "schemaVersion": 1,
4
+ "id": "example.core",
5
+ "package": "@app/module-example",
6
+ "version": "0.1.0",
7
+ "profile": "full",
8
+ "capabilities": ["api", "database", "client", "translations"],
9
+ "platform": {
10
+ "server": true,
11
+ "client": true
12
+ },
13
+ "dependencies": [
14
+ {
15
+ "id": "auth.core",
16
+ "range": "0.10.0"
17
+ }
18
+ ],
19
+ "tenancy": "required",
20
+ "locales": ["en", "pl"],
21
+ "stability": "experimental"
22
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@app/module-example",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./client": "./src/client/index.ts",
9
+ "./server": "./src/server/index.ts",
10
+ "./platform": "./src/platform.ts"
11
+ },
12
+ "scripts": {
13
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.json",
14
+ "test": "vitest run"
15
+ },
16
+ "dependencies": {
17
+ "octane": "0.1.51",
18
+ "segment-state": "0.2.0",
19
+ "@flowdular/sdk": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@tsrx/typescript-plugin": "0.3.120",
23
+ "@types/node": "24.13.3",
24
+ "typescript": "5.9.3",
25
+ "vitest": "4.1.10"
26
+ }
27
+ }
@@ -0,0 +1,43 @@
1
+ schemaVersion: 1
2
+ id: example.core
3
+ specVersion: 0.1.0
4
+ status: approved
5
+ name: Example Notes
6
+ description: A minimal tenant-scoped record module to copy when you build your first real module.
7
+ profile: full
8
+ capabilities:
9
+ - api
10
+ - database
11
+ - client
12
+ - translations
13
+ dependencies:
14
+ - id: auth.core
15
+ range: 0.10.0
16
+ tenancy: required
17
+ locales:
18
+ - en
19
+ - pl
20
+ invariants:
21
+ - Persistence runs on the shared asynchronous database contract. The module receives a leased handle from platform composition, never a driver or a connection string, and provides explicit SQL for every dialect it declares.
22
+ - The PostgreSQL notes table enables and forces row-level security with USING and WITH CHECK policies bound to transaction-local coreloom.tenant_id, and the runtime role holds neither SUPERUSER nor BYPASSRLS. Migrations use a separate lease.
23
+ - Every note is owned by exactly one tenant and every query uses the trusted tenant identifier.
24
+ permissions:
25
+ - id: example.notes.read
26
+ description: Read notes in the active tenant.
27
+ - id: example.notes.manage
28
+ description: Create notes in the active tenant.
29
+ dataOwnership:
30
+ - example.core owns note identity, title, body, and creation time.
31
+ acceptanceScenarios:
32
+ - id: EXAMPLE-LIST
33
+ given: Notes exist in more than one tenant.
34
+ when: An authorized principal lists notes.
35
+ then: Only notes owned by the active tenant are returned, newest first.
36
+ - id: EXAMPLE-CREATE
37
+ given: An authorized principal supplies a title and a body.
38
+ when: The note is created.
39
+ then: A tenant-owned note is persisted and returned with its creation time.
40
+ - id: EXAMPLE-DENY
41
+ given: A principal is unauthenticated or lacks the required example permission.
42
+ when: The principal reads or creates a note.
43
+ then: The server denies the operation before repository access.
@@ -0,0 +1,6 @@
1
+ export const EXAMPLE_PERMISSIONS = {
2
+ read: 'example.notes.read',
3
+ manage: 'example.notes.manage',
4
+ } as const;
5
+
6
+ export const permissions = Object.freeze(Object.values(EXAMPLE_PERMISSIONS));
@@ -0,0 +1,81 @@
1
+ import {
2
+ defineEndpoint,
3
+ jsonResponse,
4
+ problemResponse,
5
+ readJsonObject,
6
+ requiredString,
7
+ } from '@flowdular/sdk/server';
8
+ import type { AuthRuntime } from '@flowdular/sdk/modules/auth/server';
9
+ import {
10
+ endpointIdentityFromContext,
11
+ principalFromContext,
12
+ sessionMutationDenial,
13
+ } from '@flowdular/sdk/modules/auth/server';
14
+ import { EXAMPLE_PERMISSIONS } from '../acl/permissions.ts';
15
+ import { NoteServiceError } from '../services/note-service.ts';
16
+ import type { ExampleRuntime } from '../server/runtime.ts';
17
+
18
+ function failure(error: unknown): Response {
19
+ if (error instanceof NoteServiceError) {
20
+ return jsonResponse(
21
+ { error: { code: error.code, message: error.message } },
22
+ error.status,
23
+ );
24
+ }
25
+ return problemResponse(error, 'The example operation failed.');
26
+ }
27
+
28
+ export function createExampleRoutes(
29
+ auth: AuthRuntime,
30
+ runtime: ExampleRuntime,
31
+ ) {
32
+ const list = defineEndpoint({
33
+ id: 'example.notes.list',
34
+ path: '/api/example/notes',
35
+ methods: ['GET'],
36
+ access: { kind: 'permission', permission: EXAMPLE_PERMISSIONS.read },
37
+ resolveIdentity: endpointIdentityFromContext,
38
+ handler: async ({ octane }) => {
39
+ try {
40
+ const principal = principalFromContext(octane)!;
41
+ const service = await runtime.service();
42
+ return jsonResponse({ notes: await service.list(principal.tenantId) });
43
+ } catch (error) {
44
+ return failure(error);
45
+ }
46
+ },
47
+ });
48
+ const create = defineEndpoint({
49
+ id: 'example.notes.create',
50
+ path: '/api/example/notes',
51
+ methods: ['POST'],
52
+ access: { kind: 'permission', permission: EXAMPLE_PERMISSIONS.manage },
53
+ resolveIdentity: endpointIdentityFromContext,
54
+ handler: async ({ octane }) => {
55
+ const denial = sessionMutationDenial(octane, auth);
56
+ if (denial) return denial;
57
+ try {
58
+ const value = await readJsonObject(octane.request);
59
+ const principal = principalFromContext(octane)!;
60
+ const service = await runtime.service();
61
+ return jsonResponse(
62
+ {
63
+ note: await service.create(principal.tenantId, {
64
+ title: requiredString(value, 'title', { min: 1, max: 120 }),
65
+ body: requiredString(value, 'body', { min: 1, max: 4_000 }),
66
+ }),
67
+ },
68
+ 201,
69
+ );
70
+ } catch (error) {
71
+ return failure(error);
72
+ }
73
+ },
74
+ });
75
+ return [list.serverRoute, create.serverRoute] as const;
76
+ }
77
+
78
+ export const endpoints = [
79
+ 'example.notes.list',
80
+ 'example.notes.create',
81
+ ] as const;
@@ -0,0 +1,236 @@
1
+ import { useEffect, useMemo } from 'octane';
2
+ import {
3
+ Alert,
4
+ Button,
5
+ Drawer,
6
+ FormField,
7
+ Icon,
8
+ PageHeader,
9
+ SearchField,
10
+ TableCard,
11
+ type TableColumn,
12
+ } from '@flowdular/sdk/ui';
13
+ import { activeLocale, t } from '@flowdular/sdk/client';
14
+ import { useValue } from 'segment-state';
15
+ import type { Note } from '../domain/types.ts';
16
+ import { createNote, loadNotes } from './api.ts';
17
+ import { createExampleClientState } from './state.ts';
18
+
19
+ export interface NotesViewProps {
20
+ readonly csrfToken: string;
21
+ readonly canManage: boolean;
22
+ }
23
+
24
+ function formValue(form: FormData, key: string): string {
25
+ const value = form.get(key);
26
+ return typeof value === 'string' ? value : '';
27
+ }
28
+
29
+ function columns(): readonly TableColumn<Note>[] {
30
+ return [
31
+ {
32
+ key: 'note',
33
+ header: t('example.table.column.note'),
34
+ width: '70%',
35
+ cell: (note) => <span class="ui-cell">
36
+ <b>{note.title}</b>
37
+ <span class="ui-cell__muted">{note.body}</span>
38
+ </span>,
39
+ },
40
+ {
41
+ key: 'created',
42
+ header: t('example.table.column.created'),
43
+ width: '30%',
44
+ cell: (note) => new Intl.DateTimeFormat(activeLocale(), {
45
+ dateStyle: 'medium',
46
+ timeStyle: 'short',
47
+ }).format(new Date(note.createdAt)),
48
+ },
49
+ ];
50
+ }
51
+
52
+ export function NotesView(props: NotesViewProps) @{
53
+ const exampleState = useMemo(() => createExampleClientState(), []);
54
+ const [notes] = useValue(exampleState.state.notes);
55
+ const [status, setStatus] = useValue(exampleState.state.status);
56
+ const [error, setError] = useValue(exampleState.state.error);
57
+ const [query, setQuery] = useValue(exampleState.state.query);
58
+ const [formOpen, setFormOpen] = useValue(exampleState.state.formOpen);
59
+ const [formSession, setFormSession] = useValue(
60
+ exampleState.state.formSession,
61
+ );
62
+ const locale = activeLocale();
63
+ const term = query.trim().toLocaleLowerCase(locale);
64
+ const visible = notes.filter(
65
+ (note) =>
66
+ term === '' || note.title.toLocaleLowerCase(locale).includes(term),
67
+ );
68
+
69
+ const refresh = async () => {
70
+ setStatus('loading');
71
+ setError('');
72
+ try {
73
+ const records = await loadNotes();
74
+ exampleState.store.act(
75
+ (transaction) => transaction.set(exampleState.state.notes, records),
76
+ 'example/loaded',
77
+ );
78
+ } catch (loadError) {
79
+ setError(
80
+ loadError instanceof Error
81
+ ? loadError.message
82
+ : t('example.error.load'),
83
+ );
84
+ } finally {
85
+ setStatus('idle');
86
+ }
87
+ };
88
+
89
+ useEffect(() => {
90
+ void refresh();
91
+ }, []);
92
+
93
+ const openForm = () => {
94
+ setError('');
95
+ setFormSession((current) => current + 1);
96
+ setFormOpen(true);
97
+ };
98
+
99
+ const closeForm = () => {
100
+ setFormOpen(false);
101
+ setError('');
102
+ };
103
+
104
+ const submit = async (event: SubmitEvent) => {
105
+ event.preventDefault();
106
+ const form = new FormData(event.currentTarget as HTMLFormElement);
107
+ setStatus('submitting');
108
+ setError('');
109
+ try {
110
+ const note = await createNote(
111
+ { title: formValue(form, 'title'), body: formValue(form, 'body') },
112
+ props.csrfToken,
113
+ );
114
+ exampleState.store.act(
115
+ (transaction) => transaction.set(exampleState.state.notes, [
116
+ note,
117
+ ...notes,
118
+ ]),
119
+ 'example/created',
120
+ );
121
+ closeForm();
122
+ } catch (saveError) {
123
+ setError(
124
+ saveError instanceof Error
125
+ ? saveError.message
126
+ : t('example.error.create'),
127
+ );
128
+ } finally {
129
+ setStatus('idle');
130
+ }
131
+ };
132
+
133
+ <div class="ui-view">
134
+ <PageHeader
135
+ eyebrow={t('example.page.eyebrow')}
136
+ title={t('example.page.title')}
137
+ description={t('example.page.description')}
138
+ >
139
+ <Button size="sm" onClick={() => void refresh()}>
140
+ <Icon name="refresh" size={14} />
141
+ {t('example.page.action.refresh')}
142
+ </Button>
143
+ @if (props.canManage) {
144
+ <Button size="sm" variant="primary" onClick={openForm}>
145
+ <Icon name="plus" size={14} />
146
+ {t('example.page.action.new')}
147
+ </Button>
148
+ }
149
+ </PageHeader>
150
+ @if (error && !formOpen) {
151
+ <Alert>{error}</Alert>
152
+ }
153
+ <TableCard
154
+ title={t('example.table.title')}
155
+ count={t(
156
+ notes.length === 1
157
+ ? 'example.table.count.one'
158
+ : 'example.table.count.other',
159
+ { count: notes.length },
160
+ )}
161
+ search={<SearchField
162
+ value={query}
163
+ placeholder={t('example.table.search.placeholder')}
164
+ label={t('example.table.search.label')}
165
+ onInput={setQuery}
166
+ />}
167
+ caption={t('example.table.caption')}
168
+ columns={columns()}
169
+ rows={visible}
170
+ rowKey={(note) => note.id}
171
+ status={status === 'loading' && notes.length === 0 ? 'loading' : 'idle'}
172
+ loadingLabel={t('example.table.loading')}
173
+ filtered={term !== ''}
174
+ empty={{
175
+ icon: 'file-text',
176
+ title: t('example.table.empty.title'),
177
+ hint: t('example.table.empty.hint'),
178
+ }}
179
+ emptyFiltered={{
180
+ icon: 'file-text',
181
+ title: t('example.table.emptyFiltered.title'),
182
+ hint: t('example.table.emptyFiltered.hint'),
183
+ }}
184
+ note={t('example.table.note')}
185
+ />
186
+ <Drawer
187
+ open={formOpen}
188
+ title={t('example.drawer.title')}
189
+ subtitle={t('example.drawer.subtitle')}
190
+ onClose={closeForm}
191
+ >
192
+ <form
193
+ class="ui-drawer__form"
194
+ key={'example-form-' + formSession}
195
+ onSubmit={submit}
196
+ >
197
+ <div class="ui-drawer__body">
198
+ @if (error) {
199
+ <Alert>{error}</Alert>
200
+ }
201
+ <FormField label={t('example.field.title')}>
202
+ <input
203
+ class="ui-input"
204
+ id="title"
205
+ name="title"
206
+ required
207
+ maxLength={120}
208
+ />
209
+ </FormField>
210
+ <FormField
211
+ label={t('example.field.body')}
212
+ help={t('example.field.body.help')}
213
+ >
214
+ <textarea
215
+ class="ui-textarea"
216
+ id="body"
217
+ required
218
+ rows={6}
219
+ maxLength={4000}
220
+ ></textarea>
221
+ </FormField>
222
+ </div>
223
+ <div class="ui-drawer__foot">
224
+ <Button type="button" onClick={closeForm}>{t(
225
+ 'example.drawer.cancel',
226
+ )}</Button>
227
+ <Button
228
+ type="submit"
229
+ variant="primary"
230
+ disabled={status === 'submitting'}
231
+ >{t('example.drawer.save')}</Button>
232
+ </div>
233
+ </form>
234
+ </Drawer>
235
+ </div>
236
+ }
@@ -0,0 +1,38 @@
1
+ import { t } from '@flowdular/sdk/client/i18n';
2
+ import type { CreateNoteInput, Note } from '../domain/types.ts';
3
+
4
+ interface ErrorEnvelope {
5
+ readonly error?: { readonly message?: string };
6
+ }
7
+
8
+ async function payload<T>(response: Response): Promise<T> {
9
+ const value = (await response.json()) as T & ErrorEnvelope;
10
+ if (!response.ok) {
11
+ throw new Error(value.error?.message ?? t('example.error.request'));
12
+ }
13
+ return value;
14
+ }
15
+
16
+ export async function loadNotes(): Promise<readonly Note[]> {
17
+ const response = await fetch('/api/example/notes', {
18
+ headers: { accept: 'application/json' },
19
+ credentials: 'same-origin',
20
+ });
21
+ return (await payload<{ readonly notes: readonly Note[] }>(response)).notes;
22
+ }
23
+
24
+ export async function createNote(
25
+ input: CreateNoteInput,
26
+ csrfToken: string,
27
+ ): Promise<Note> {
28
+ const response = await fetch('/api/example/notes', {
29
+ method: 'POST',
30
+ headers: {
31
+ 'content-type': 'application/json',
32
+ 'x-csrf-token': csrfToken,
33
+ },
34
+ credentials: 'same-origin',
35
+ body: JSON.stringify(input),
36
+ });
37
+ return (await payload<{ readonly note: Note }>(response)).note;
38
+ }
@@ -0,0 +1,52 @@
1
+ import type { ModuleClientContribution } from '@flowdular/sdk/client';
2
+ import { EXAMPLE_PERMISSIONS } from '../acl/permissions.ts';
3
+ import translationsEn from '../../translations/en.json';
4
+ import translationsPl from '../../translations/pl.json';
5
+ import { NotesView } from './NotesView.tsrx';
6
+ import {
7
+ exampleNavigationDescription,
8
+ exampleNavigationLabel,
9
+ } from './navigation-copy.ts';
10
+
11
+ export interface ExampleClientContributionOptions {
12
+ readonly csrfToken: string;
13
+ readonly scopes: readonly string[];
14
+ }
15
+
16
+ export function createExampleClientContribution(
17
+ options: ExampleClientContributionOptions,
18
+ ): ModuleClientContribution {
19
+ return {
20
+ moduleId: 'example.core',
21
+ translations: { en: translationsEn, pl: translationsPl },
22
+ navigation: [
23
+ {
24
+ id: 'example.navigation',
25
+ viewId: 'example',
26
+ group: 'Operations',
27
+ /* The shell registers this contribution's bundles after the factory
28
+ returns. Getters resolve copy only when the shell reads navigation,
29
+ after registration, and again when the active locale changes. */
30
+ get label() {
31
+ return exampleNavigationLabel();
32
+ },
33
+ glyph: 'file-text',
34
+ get description() {
35
+ return exampleNavigationDescription();
36
+ },
37
+ scope: EXAMPLE_PERMISSIONS.read,
38
+ order: 20,
39
+ },
40
+ ],
41
+ views: [
42
+ {
43
+ id: 'example',
44
+ render: () =>
45
+ <NotesView
46
+ csrfToken={options.csrfToken}
47
+ canManage={options.scopes.includes(EXAMPLE_PERMISSIONS.manage)}
48
+ />,
49
+ },
50
+ ],
51
+ };
52
+ }
@@ -0,0 +1,18 @@
1
+ export { createExampleClientContribution } from './contribution.tsrx';
2
+ export type { ExampleClientContributionOptions } from './contribution.tsrx';
3
+ export { NotesView } from './NotesView.tsrx';
4
+ import type {
5
+ ModuleClientContext,
6
+ ModuleClientContribution,
7
+ } from '@flowdular/sdk/client';
8
+ import { createExampleClientContribution as canonicalContribution } from './contribution.tsrx';
9
+
10
+ /* Canonical entry used by the generated platform composition. */
11
+ export function createClientContribution(
12
+ context: ModuleClientContext,
13
+ ): ModuleClientContribution {
14
+ return canonicalContribution({
15
+ csrfToken: context.csrfToken,
16
+ scopes: context.scopes,
17
+ });
18
+ }
@@ -0,0 +1,9 @@
1
+ import { t } from '@flowdular/sdk/client/i18n';
2
+
3
+ export function exampleNavigationLabel(): string {
4
+ return t('example.nav.label');
5
+ }
6
+
7
+ export function exampleNavigationDescription(): string {
8
+ return t('example.nav.description');
9
+ }
@@ -0,0 +1,14 @@
1
+ import { cell, createStore } from 'segment-state';
2
+ import type { Note } from '../domain/types.ts';
3
+
4
+ export function createExampleClientState() {
5
+ const store = createStore({
6
+ notes: cell<readonly Note[]>([]),
7
+ query: '',
8
+ formOpen: false,
9
+ status: cell<'idle' | 'loading' | 'submitting'>('idle'),
10
+ error: '',
11
+ formSession: 0,
12
+ });
13
+ return { store, state: store.state };
14
+ }
@@ -0,0 +1,12 @@
1
+ export interface Note {
2
+ readonly tenantId: string;
3
+ readonly id: string;
4
+ readonly title: string;
5
+ readonly body: string;
6
+ readonly createdAt: number;
7
+ }
8
+
9
+ export interface CreateNoteInput {
10
+ readonly title: string;
11
+ readonly body: string;
12
+ }
@@ -0,0 +1,24 @@
1
+ import type {
2
+ ModuleManifest,
3
+ RegisteredModule,
4
+ } from '@flowdular/sdk/contracts';
5
+ import manifest from '../module.json' with { type: 'json' };
6
+ import { EXAMPLE_PERMISSIONS } from './acl/permissions.ts';
7
+
8
+ export const moduleDefinition = {
9
+ manifest: manifest as ModuleManifest,
10
+ navigation: [
11
+ {
12
+ id: 'example.navigation',
13
+ label: 'Notes',
14
+ href: '/example',
15
+ order: 20,
16
+ permission: EXAMPLE_PERMISSIONS.read,
17
+ },
18
+ ],
19
+ permissions: Object.values(EXAMPLE_PERMISSIONS),
20
+ } satisfies RegisteredModule;
21
+
22
+ export { EXAMPLE_PERMISSIONS } from './acl/permissions.ts';
23
+ export { NoteService, NoteServiceError } from './services/note-service.ts';
24
+ export type { CreateNoteInput, Note } from './domain/types.ts';
@@ -0,0 +1,23 @@
1
+ import type {
2
+ PlatformServerComposition,
3
+ PlatformServerContext,
4
+ } from '@flowdular/sdk/modules/auth/server';
5
+ import { createExampleRoutes, createExampleRuntime } from './server/index.ts';
6
+
7
+ export function createServerComposition(
8
+ context: PlatformServerContext,
9
+ ): PlatformServerComposition {
10
+ const runtime = createExampleRuntime({
11
+ databases: context.databases,
12
+ purpose:
13
+ context.environment.NODE_ENV === 'test'
14
+ ? 'test'
15
+ : context.environment.NODE_ENV === 'production'
16
+ ? 'runtime'
17
+ : 'preview',
18
+ });
19
+ return {
20
+ routes: createExampleRoutes(context.auth, runtime),
21
+ dispose: () => runtime.dispose(),
22
+ };
23
+ }
@@ -0,0 +1,3 @@
1
+ export { createExampleRoutes } from '../api/endpoints.ts';
2
+ export { createExampleRuntime } from './runtime.ts';
3
+ export type { ExampleRuntime, ExampleRuntimeOptions } from './runtime.ts';