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,84 @@
1
+ import type {
2
+ DatabaseAdapterLease,
3
+ DatabaseProvider,
4
+ DatabaseProviderRequest,
5
+ } from '@flowdular/sdk/database';
6
+ import {
7
+ DATABASE_CAPABILITY_IDS,
8
+ DATABASE_DIALECT_IDS,
9
+ } from '@flowdular/sdk/database';
10
+ import {
11
+ DatabaseNoteRepository,
12
+ migrateExampleDatabase,
13
+ } from '../services/database-repository.ts';
14
+ import { NoteService } from '../services/note-service.ts';
15
+
16
+ export interface ExampleRuntimeOptions {
17
+ readonly databases: DatabaseProvider;
18
+ readonly purpose: Exclude<DatabaseProviderRequest['purpose'], 'migration'>;
19
+ }
20
+
21
+ export interface ExampleRuntime {
22
+ service(): Promise<NoteService>;
23
+ dispose(): Promise<void>;
24
+ }
25
+
26
+ export function createExampleRuntime(
27
+ options: ExampleRuntimeOptions,
28
+ ): ExampleRuntime {
29
+ let disposed = false;
30
+ let runtimeLeasePromise: Promise<DatabaseAdapterLease> | undefined;
31
+ let servicePromise: Promise<NoteService> | undefined;
32
+
33
+ const initialize = async (): Promise<NoteService> => {
34
+ const migrationLease = await options.databases.acquire({
35
+ namespace: 'example.core',
36
+ purpose: 'migration',
37
+ requirements: {
38
+ dialectIds: [DATABASE_DIALECT_IDS.postgresql],
39
+ capabilities: [
40
+ DATABASE_CAPABILITY_IDS.MIGRATION_LOCK,
41
+ DATABASE_CAPABILITY_IDS.SCHEMA_INTROSPECTION,
42
+ DATABASE_CAPABILITY_IDS.TRANSACTIONAL_DDL,
43
+ ],
44
+ },
45
+ });
46
+ try {
47
+ await migrateExampleDatabase(migrationLease.database);
48
+ } finally {
49
+ await migrationLease.release();
50
+ }
51
+ runtimeLeasePromise = options.databases.acquire({
52
+ namespace: 'example.core',
53
+ purpose: options.purpose,
54
+ requirements: {
55
+ dialectIds: [DATABASE_DIALECT_IDS.postgresql],
56
+ capabilities: [DATABASE_CAPABILITY_IDS.TRANSACTIONS],
57
+ },
58
+ });
59
+ const lease = await runtimeLeasePromise;
60
+ return new NoteService(new DatabaseNoteRepository(lease.database));
61
+ };
62
+
63
+ return {
64
+ service: () => {
65
+ if (disposed) {
66
+ return Promise.reject(new Error('Example runtime is disposed.'));
67
+ }
68
+ servicePromise ??= initialize();
69
+ return servicePromise;
70
+ },
71
+ async dispose() {
72
+ if (disposed) return;
73
+ disposed = true;
74
+ if (!runtimeLeasePromise) {
75
+ await servicePromise?.catch(() => undefined);
76
+ }
77
+ if (!runtimeLeasePromise) return;
78
+ const lease = await runtimeLeasePromise;
79
+ await lease.release();
80
+ runtimeLeasePromise = undefined;
81
+ servicePromise = undefined;
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,82 @@
1
+ import type { DatabaseHandle } from '@flowdular/sdk/database';
2
+ import { runDatabaseMigrations } from '@flowdular/sdk/database';
3
+ import type { Note } from '../domain/types.ts';
4
+ import { databaseMigrations } from './migration.ts';
5
+ import type { NoteRepository } from './repository.ts';
6
+
7
+ interface NoteRow {
8
+ tenant_id: string;
9
+ id: string;
10
+ title: string;
11
+ body: string;
12
+ created_at: number | bigint | string;
13
+ }
14
+
15
+ /* Queries stay explicit. Note data never passes through a SQL rewriter, and
16
+ values always use the adapter's parameter channel. */
17
+ const LIST = `SELECT tenant_id, id, title, body, created_at
18
+ FROM example_notes
19
+ WHERE tenant_id = $1
20
+ ORDER BY created_at DESC, id ASC`;
21
+
22
+ const CREATE = `INSERT INTO example_notes
23
+ (tenant_id, id, title, body, created_at)
24
+ VALUES ($1, $2, $3, $4, $5)`;
25
+
26
+ /* PostgreSQL returns BIGINT as a string, so every numeric read is normalized
27
+ before it reaches the domain. */
28
+ function integer(value: NoteRow['created_at']): number {
29
+ const normalized = Number(value);
30
+ if (!Number.isSafeInteger(normalized) || normalized < 0) {
31
+ throw new Error('The example database returned an invalid timestamp.');
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ function fromRow(row: NoteRow): Note {
37
+ return {
38
+ tenantId: row.tenant_id,
39
+ id: row.id,
40
+ title: row.title,
41
+ body: row.body,
42
+ createdAt: integer(row.created_at),
43
+ };
44
+ }
45
+
46
+ /** A repository over a platform-owned PostgreSQL handle. */
47
+ export class DatabaseNoteRepository implements NoteRepository {
48
+ constructor(private readonly database: DatabaseHandle) {}
49
+
50
+ async list(tenantId: string): Promise<readonly Note[]> {
51
+ const result = await this.database.transaction(
52
+ (transaction) =>
53
+ transaction.query<NoteRow>({ text: LIST, parameters: [tenantId] }),
54
+ { access: 'read', tenantId },
55
+ );
56
+ return result.rows.map(fromRow);
57
+ }
58
+
59
+ async create(note: Note): Promise<Note> {
60
+ await this.database.transaction(
61
+ (transaction) =>
62
+ transaction.execute({
63
+ text: CREATE,
64
+ parameters: [
65
+ note.tenantId,
66
+ note.id,
67
+ note.title,
68
+ note.body,
69
+ note.createdAt,
70
+ ],
71
+ }),
72
+ { access: 'write', tenantId: note.tenantId },
73
+ );
74
+ return note;
75
+ }
76
+ }
77
+
78
+ export async function migrateExampleDatabase(
79
+ database: DatabaseHandle,
80
+ ): Promise<void> {
81
+ await runDatabaseMigrations(database, 'example.core', databaseMigrations);
82
+ }
@@ -0,0 +1,6 @@
1
+ export { NoteService, NoteServiceError } from './note-service.ts';
2
+ export type { NoteRepository } from './repository.ts';
3
+ export {
4
+ DatabaseNoteRepository,
5
+ migrateExampleDatabase,
6
+ } from './database-repository.ts';
@@ -0,0 +1,35 @@
1
+ import type { DatabaseMigration } from '@flowdular/sdk/database';
2
+ import { postgresTenantTableState } from '@flowdular/sdk/database';
3
+
4
+ /* Every constant mirrors its migrations/<id>.up.sql file byte for byte;
5
+ tests/migrations.test.ts fails on drift. */
6
+
7
+ export const EXAMPLE_MIGRATION_001 = `CREATE TABLE IF NOT EXISTS example_notes (
8
+ tenant_id TEXT NOT NULL,
9
+ id TEXT NOT NULL,
10
+ title TEXT NOT NULL,
11
+ body TEXT NOT NULL,
12
+ created_at BIGINT NOT NULL,
13
+ PRIMARY KEY (tenant_id, id)
14
+ );
15
+ CREATE INDEX IF NOT EXISTS example_notes_tenant_created_idx ON example_notes (tenant_id, created_at DESC);
16
+ ALTER TABLE example_notes ENABLE ROW LEVEL SECURITY;
17
+ ALTER TABLE example_notes FORCE ROW LEVEL SECURITY;
18
+ CREATE POLICY example_notes_tenant_policy ON example_notes
19
+ USING (tenant_id = current_setting('coreloom.tenant_id', true))
20
+ WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
21
+ `;
22
+
23
+ export const databaseMigrations: readonly DatabaseMigration[] = [
24
+ {
25
+ id: '0001_example_core',
26
+ sql: { postgresql: EXAMPLE_MIGRATION_001 },
27
+ inspectExisting: (database) =>
28
+ postgresTenantTableState(
29
+ database,
30
+ 'example_notes',
31
+ 'example_notes_tenant_policy',
32
+ [() => database.schema.hasIndex('example_notes_tenant_created_idx')],
33
+ ),
34
+ },
35
+ ];
@@ -0,0 +1,54 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { CreateNoteInput, Note } from '../domain/types.ts';
3
+ import type { NoteRepository } from './repository.ts';
4
+
5
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
6
+
7
+ export class NoteServiceError extends Error {
8
+ constructor(
9
+ readonly code: string,
10
+ message: string,
11
+ readonly status = 400,
12
+ ) {
13
+ super(message);
14
+ this.name = 'NoteServiceError';
15
+ }
16
+ }
17
+
18
+ function text(value: string, field: string, max: number): string {
19
+ if (typeof value !== 'string' || CONTROL_CHARACTER_PATTERN.test(value)) {
20
+ throw new NoteServiceError(
21
+ 'INVALID_INPUT',
22
+ `${field} must not contain control characters.`,
23
+ );
24
+ }
25
+ const normalized = value.trim();
26
+ if (normalized.length < 1 || normalized.length > max) {
27
+ throw new NoteServiceError(
28
+ 'INVALID_INPUT',
29
+ `${field} must contain between 1 and ${String(max)} characters.`,
30
+ );
31
+ }
32
+ return normalized;
33
+ }
34
+
35
+ export class NoteService {
36
+ constructor(
37
+ private readonly repository: NoteRepository,
38
+ private readonly now: () => number = () => Date.now(),
39
+ ) {}
40
+
41
+ list(tenantId: string): Promise<readonly Note[]> {
42
+ return this.repository.list(tenantId);
43
+ }
44
+
45
+ create(tenantId: string, input: CreateNoteInput): Promise<Note> {
46
+ return this.repository.create({
47
+ tenantId,
48
+ id: randomUUID(),
49
+ title: text(input.title, 'title', 120),
50
+ body: text(input.body, 'body', 4_000),
51
+ createdAt: this.now(),
52
+ });
53
+ }
54
+ }
@@ -0,0 +1,7 @@
1
+ import type { Note } from '../domain/types.ts';
2
+
3
+ /** The database-agnostic business port. No driver type crosses it. */
4
+ export interface NoteRepository {
5
+ list(tenantId: string): Promise<readonly Note[]>;
6
+ create(note: Note): Promise<Note>;
7
+ }
@@ -0,0 +1,67 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import manifest from '../module.json' with { type: 'json' };
4
+ import translationsEn from '../translations/en.json' with { type: 'json' };
5
+ import translationsPl from '../translations/pl.json' with { type: 'json' };
6
+ import { EXAMPLE_PERMISSIONS, permissions } from '../src/acl/permissions.ts';
7
+ import { endpoints } from '../src/api/endpoints.ts';
8
+ import { databaseMigrations } from '../src/services/migration.ts';
9
+
10
+ const migrationDirectory = new URL('../migrations/', import.meta.url);
11
+
12
+ describe('example.core manifest', () => {
13
+ it('declares the identifier the platform composes', () => {
14
+ expect(manifest.id).toBe('example.core');
15
+ expect(manifest.package).toBe('@app/module-example');
16
+ expect(manifest.tenancy).toBe('required');
17
+ });
18
+
19
+ it('declares one permission per action the API enforces', () => {
20
+ expect([...permissions]).toEqual([
21
+ 'example.notes.read',
22
+ 'example.notes.manage',
23
+ ]);
24
+ expect(EXAMPLE_PERMISSIONS.read).toBe('example.notes.read');
25
+ expect(endpoints).toEqual(['example.notes.list', 'example.notes.create']);
26
+ });
27
+ });
28
+
29
+ describe('example.core translations', () => {
30
+ it('ships the same key set in every declared locale', () => {
31
+ expect(Object.keys(translationsPl).sort()).toEqual(
32
+ Object.keys(translationsEn).sort(),
33
+ );
34
+ expect(manifest.locales).toEqual(['en', 'pl']);
35
+ });
36
+
37
+ it('never ships an empty string', () => {
38
+ for (const bundle of [translationsEn, translationsPl]) {
39
+ for (const [key, value] of Object.entries(bundle)) {
40
+ expect(value.trim(), key).not.toBe('');
41
+ }
42
+ }
43
+ });
44
+ });
45
+
46
+ describe('example.core migrations', () => {
47
+ it('mirrors every PostgreSQL up file byte for byte', () => {
48
+ for (const migration of databaseMigrations) {
49
+ expect(migration.sql.postgresql).toBe(
50
+ readFileSync(
51
+ new URL(`${migration.id}.up.sql`, migrationDirectory),
52
+ 'utf8',
53
+ ),
54
+ );
55
+ }
56
+ });
57
+
58
+ it('declares forced row security for every tenant table', () => {
59
+ for (const migration of databaseMigrations) {
60
+ const sql = migration.sql.postgresql ?? '';
61
+ expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
62
+ expect(sql).toContain('FORCE ROW LEVEL SECURITY');
63
+ expect(sql).toContain("current_setting('coreloom.tenant_id', true)");
64
+ expect(sql).toContain('WITH CHECK');
65
+ }
66
+ });
67
+ });
@@ -0,0 +1,83 @@
1
+ import { afterAll, afterEach, describe, expect, it } from 'vitest';
2
+ import { NoteService, NoteServiceError } from '../src/services/note-service.ts';
3
+ import {
4
+ closeExampleTestDatabases,
5
+ createExampleTestDatabase,
6
+ type ExampleTestDatabase,
7
+ } from './support/database.ts';
8
+
9
+ const databases = new Set<ExampleTestDatabase>();
10
+
11
+ afterEach(async () => {
12
+ await Promise.all([...databases].map((database) => database.dispose()));
13
+ databases.clear();
14
+ });
15
+
16
+ afterAll(closeExampleTestDatabases);
17
+
18
+ async function fixture(): Promise<ExampleTestDatabase> {
19
+ const database = await createExampleTestDatabase();
20
+ databases.add(database);
21
+ return database;
22
+ }
23
+
24
+ describe('example notes', () => {
25
+ it('lists only the notes the active tenant owns', async () => {
26
+ const service = new NoteService((await fixture()).repository);
27
+ await service.create('tenant-a', { title: 'A', body: 'Owned by A' });
28
+ await service.create('tenant-b', { title: 'B', body: 'Owned by B' });
29
+
30
+ expect((await service.list('tenant-a')).map((note) => note.title)).toEqual([
31
+ 'A',
32
+ ]);
33
+ expect((await service.list('tenant-b')).map((note) => note.title)).toEqual([
34
+ 'B',
35
+ ]);
36
+ });
37
+
38
+ it('normalizes the BIGINT timestamp PostgreSQL returns as a string', async () => {
39
+ const service = new NoteService((await fixture()).repository);
40
+ const created = await service.create('tenant-a', {
41
+ title: 'Timestamps',
42
+ body: 'One note',
43
+ });
44
+
45
+ const [read] = await service.list('tenant-a');
46
+ expect(read?.createdAt).toBe(created.createdAt);
47
+ expect(typeof read?.createdAt).toBe('number');
48
+ });
49
+
50
+ it('returns the newest note first', async () => {
51
+ let clock = 1_000;
52
+ const service = new NoteService(
53
+ (await fixture()).repository,
54
+ () => (clock += 1_000),
55
+ );
56
+ await service.create('tenant-a', { title: 'First', body: 'x' });
57
+ await service.create('tenant-a', { title: 'Second', body: 'y' });
58
+
59
+ expect((await service.list('tenant-a')).map((note) => note.title)).toEqual([
60
+ 'Second',
61
+ 'First',
62
+ ]);
63
+ });
64
+
65
+ it('refuses a query that carries no tenant context', async () => {
66
+ const database = await fixture();
67
+
68
+ await expect(
69
+ database.runtime.query({ text: 'SELECT 1 FROM example_notes' }),
70
+ ).rejects.toMatchObject({ code: 'TENANT_CONTEXT_REQUIRED' });
71
+ });
72
+
73
+ it('rejects an empty title and a title longer than the column allows', async () => {
74
+ const service = new NoteService((await fixture()).repository);
75
+
76
+ await expect(async () =>
77
+ service.create('tenant-a', { title: ' ', body: 'x' }),
78
+ ).rejects.toBeInstanceOf(NoteServiceError);
79
+ await expect(async () =>
80
+ service.create('tenant-a', { title: 'x'.repeat(121), body: 'x' }),
81
+ ).rejects.toBeInstanceOf(NoteServiceError);
82
+ });
83
+ });
@@ -0,0 +1,69 @@
1
+ import type { DatabaseHandle, DatabaseProvider } from '@flowdular/sdk/database';
2
+ import { createPgliteTestProvider } from '@flowdular/sdk/database-testing';
3
+ import {
4
+ DatabaseNoteRepository,
5
+ migrateExampleDatabase,
6
+ } from '../../src/services/database-repository.ts';
7
+
8
+ export interface ExampleTestDatabase {
9
+ readonly provider: DatabaseProvider;
10
+ readonly runtime: DatabaseHandle;
11
+ readonly repository: DatabaseNoteRepository;
12
+ dispose(): Promise<void>;
13
+ }
14
+
15
+ /* Booting an embedded PostgreSQL costs about two seconds, so a test file shares
16
+ one migrated cluster and every fixture starts from truncated tables instead.
17
+ Call closeExampleTestDatabases() from the file's afterAll. */
18
+ let shared: Promise<DatabaseProvider> | undefined;
19
+
20
+ /** The file's migrated provider, with every example table emptied. */
21
+ export async function exampleTestProvider(): Promise<DatabaseProvider> {
22
+ shared ??= (async () => {
23
+ const provider = createPgliteTestProvider();
24
+ const lease = await provider.acquire({
25
+ namespace: 'example.core',
26
+ purpose: 'migration',
27
+ });
28
+ try {
29
+ await migrateExampleDatabase(lease.database);
30
+ } finally {
31
+ await lease.release();
32
+ }
33
+ return provider;
34
+ })();
35
+ const provider = await shared;
36
+ const migration = await provider.acquire({
37
+ namespace: 'example.core',
38
+ purpose: 'migration',
39
+ });
40
+ try {
41
+ await migration.database.execute({
42
+ text: 'TRUNCATE example_notes RESTART IDENTITY CASCADE',
43
+ });
44
+ } finally {
45
+ await migration.release();
46
+ }
47
+ return provider;
48
+ }
49
+
50
+ /** An empty example database with a tenant-scoped runtime handle. */
51
+ export async function createExampleTestDatabase(): Promise<ExampleTestDatabase> {
52
+ const provider = await exampleTestProvider();
53
+ const lease = await provider.acquire({
54
+ namespace: 'example.core',
55
+ purpose: 'test',
56
+ });
57
+ return {
58
+ provider,
59
+ runtime: lease.database,
60
+ repository: new DatabaseNoteRepository(lease.database),
61
+ dispose: () => lease.release(),
62
+ };
63
+ }
64
+
65
+ export async function closeExampleTestDatabases(): Promise<void> {
66
+ const provider = shared;
67
+ shared = undefined;
68
+ if (provider) await (await provider).dispose();
69
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "module.name": "Example Notes",
3
+ "nav.label": "Notes",
4
+ "nav.description": "Tenant-scoped notes",
5
+ "page.eyebrow": "Example",
6
+ "page.title": "Notes",
7
+ "page.description": "A tenant-scoped record screen to copy when you build your first module.",
8
+ "page.action.refresh": "Refresh",
9
+ "page.action.new": "New note",
10
+ "table.title": "Notes",
11
+ "table.count.one": "{count} note",
12
+ "table.count.other": "{count} notes",
13
+ "table.column.note": "Note",
14
+ "table.column.created": "Created",
15
+ "table.search.placeholder": "Search notes",
16
+ "table.search.label": "Search notes",
17
+ "table.caption": "Notes owned by the active workspace",
18
+ "table.loading": "Loading notes",
19
+ "table.empty.title": "No notes yet",
20
+ "table.empty.hint": "Create the first note to see the screen with data.",
21
+ "table.emptyFiltered.title": "No note matches",
22
+ "table.emptyFiltered.hint": "Clear the search to see every note.",
23
+ "table.note": "Notes belong to the active workspace and are never visible to another one.",
24
+ "drawer.title": "New note",
25
+ "drawer.subtitle": "Stored in the active workspace",
26
+ "drawer.cancel": "Cancel",
27
+ "drawer.save": "Create note",
28
+ "field.title": "Title",
29
+ "field.body": "Body",
30
+ "field.body.help": "Up to 4000 characters.",
31
+ "error.load": "Could not load notes.",
32
+ "error.create": "Could not create the note.",
33
+ "error.request": "The example operation failed."
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "module.name": "Przykładowe notatki",
3
+ "nav.label": "Notatki",
4
+ "nav.description": "Notatki w ramach przestrzeni",
5
+ "page.eyebrow": "Przykład",
6
+ "page.title": "Notatki",
7
+ "page.description": "Ekran rekordów w ramach przestrzeni, od którego zaczniesz swój pierwszy moduł.",
8
+ "page.action.refresh": "Odśwież",
9
+ "page.action.new": "Nowa notatka",
10
+ "table.title": "Notatki",
11
+ "table.count.one": "{count} notatka",
12
+ "table.count.other": "{count} notatek",
13
+ "table.column.note": "Notatka",
14
+ "table.column.created": "Utworzono",
15
+ "table.search.placeholder": "Szukaj notatek",
16
+ "table.search.label": "Szukaj notatek",
17
+ "table.caption": "Notatki należące do aktywnej przestrzeni",
18
+ "table.loading": "Wczytywanie notatek",
19
+ "table.empty.title": "Nie ma jeszcze notatek",
20
+ "table.empty.hint": "Utwórz pierwszą notatkę, aby zobaczyć ekran z danymi.",
21
+ "table.emptyFiltered.title": "Brak pasujących notatek",
22
+ "table.emptyFiltered.hint": "Wyczyść wyszukiwanie, aby zobaczyć wszystkie notatki.",
23
+ "table.note": "Notatki należą do aktywnej przestrzeni i nigdy nie są widoczne w innej.",
24
+ "drawer.title": "Nowa notatka",
25
+ "drawer.subtitle": "Zapisywana w aktywnej przestrzeni",
26
+ "drawer.cancel": "Anuluj",
27
+ "drawer.save": "Utwórz notatkę",
28
+ "field.title": "Tytuł",
29
+ "field.body": "Treść",
30
+ "field.body.help": "Do 4000 znaków.",
31
+ "error.load": "Nie udało się wczytać notatek.",
32
+ "error.create": "Nie udało się utworzyć notatki.",
33
+ "error.request": "Operacja przykładowego modułu nie powiodła się."
34
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "isolatedModules": true,
9
+ "jsx": "react-jsx",
10
+ "jsxImportSource": "octane",
11
+ "plugins": [{ "name": "@tsrx/typescript-plugin" }]
12
+ },
13
+ "include": ["src/**/*", "tests/**/*.ts"]
14
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ /* Booting the embedded PostgreSQL a suite runs against takes seconds under
4
+ parallel load, well past the 5s vitest default. */
5
+ export default defineConfig({
6
+ test: {
7
+ testTimeout: 30_000,
8
+ hookTimeout: 30_000,
9
+ },
10
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "flowdular-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.17.0",
7
+ "engines": {
8
+ "node": ">=22.22.2"
9
+ },
10
+ "scripts": {
11
+ "dev": "node --env-file-if-exists=.env platform/scripts/dev.mjs",
12
+ "typecheck": "pnpm -r --if-present typecheck",
13
+ "test": "pnpm -r --if-present test",
14
+ "format": "prettier --write .",
15
+ "format:check": "prettier --check .",
16
+ "flowdular": "flowdular",
17
+ "cl": "flowdular",
18
+ "doctor": "flowdular doctor",
19
+ "verify": "pnpm typecheck && pnpm test && flowdular module validate && pnpm format:check"
20
+ },
21
+ "devDependencies": {
22
+ "@tsrx/prettier-plugin": "0.3.120",
23
+ "prettier": "3.6.2",
24
+ "flowdular": "0.1.0"
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#141B2E" />
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <!--ssr-head-->
9
+ </head>
10
+ <body>
11
+ <div id="root"><!--ssr-body--></div>
12
+ </body>
13
+ </html>