Keep one workspace’s notes separate from another’s

Run a small database workflow: create a note, protect ownership, list the right records, reject invalid input and roll back a failed write.

On this pageSource-backed Markdown

What we are building

A note belongs to a workspace. Request data may change its title and body, but cannot move it to another workspace. Reading, editing and deleting always includes the trusted workspace identity.

This is a runnable database lab, not a complete SaaS starter. It combines App, ActiveRecord, fields and transactions. Your application still owns authentication and membership checks. We deliberately pass an already-authorized workspace into the feature functions.

Install the example

Use Node.js 24, npm, and a running MariaDB/MySQL server. Use a test account with permission to create and drop db3_app_test_* databases. The script creates a uniquely named database and removes it in finally; it does not use your application database. If the process is killed, inspect the exact leftover test database before removing it.

The framework is pre-release. Obtain the matching @db3.ai/pure and @db3.ai/app tarballs from the framework maintainer before starting. Replace the two /path/to paths below with those files. There is no public npm initializer to substitute at this stage.

Run these commands in a new directory. They copy the actual shipped example files, so you can edit and rerun them without relying on a sibling repository.

Run in a new directory
bash
mkdir db3-notes
cd db3-notes
npm init -y
npm pkg set type=module
npm install /path/to/db3.ai-pure-0.1.0.tgz /path/to/db3.ai-app-0.1.0.tgz
npm install --save-dev tsx@^4 typescript@^6 @types/node@^24 vitest@^4
mkdir examples
cp node_modules/@db3.ai/app/src/db/examples/KnowledgeNote.ts examples/
cp node_modules/@db3.ai/app/src/db/examples/workspaceNotes.ts examples/
cp node_modules/@db3.ai/app/src/db/examples/createNotesTogether.ts examples/
cp node_modules/@db3.ai/app/src/db/examples/runWorkspaceNotes.ts examples/

Use a test database account

Create a .env file in that directory with your local test connection. Replace the placeholders; do not commit credentials. DB_DATABASE and the prefix below identify the test namespace, not a database the script assumes already exists.

Access denied means the account or authentication method is wrong. Connection refused means the server/port is unavailable. This lab uses TCP credentials; a socket-only local account is not enough. Creating a database also needs the appropriate database privileges.

.env (local test credentials)
bash
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=your_test_user
DB_PASSWORD=your_test_password
DB_DATABASE=db3_app_test
DB_TEST_DATABASE_PREFIX=db3_app_test

Run it and inspect the result

Run this from the directory containing .env. The output below is checked by the example test. IDs and timestamps are omitted so you can compare the result directly.

The script adds a second workspace, attempts to inject its identity into a note, checks validation, then makes a two-note transaction fail. Only the authorized note is listed, and the failed transaction leaves the previous count unchanged.

Run the walkthrough
bash
npx tsx examples/runWorkspaceNotes.ts
Tested output
json
{
	"title": "Client brief",
	"visibleNotes": 1,
	"workspaceProtected": true,
	"jsonTimestamp": true,
	"validationCodes": ["required"],
	"rollbackPreservedCount": true,
	"remainingNotes": 0
}

Read the complete runner

This is the script you just ran. It uses the model and note functions from the ActiveRecord guide, then cleans up its test database.

runWorkspaceNotes.ts
ts
import { pathToFileURL } from 'node:url';
import { RecordValidationError } from '@db3.ai/app/db';
import { createGeneratedTestDatabase } from '@db3.ai/app/db/test/db';
import { App } from '@db3.ai/app/server';
import { createNotesTogether } from './createNotesTogether';
import { KnowledgeNote } from './KnowledgeNote';
import { createWorkspaceNote, deleteWorkspaceNote, listWorkspaceNotes, updateWorkspaceNote } from './workspaceNotes';

/** Observable, deterministic outcomes from the disposable notes walkthrough. */
export interface WorkspaceNotesOutcome {
	/** Trimmed title read back through the model query. */
	title: string | null;
	/** Number of notes visible in the authorized workspace. */
	visibleNotes: number;
	/** Whether request filling refused a payload-supplied ownership change. */
	workspaceProtected: boolean;
	/** Whether transport conversion rendered the timestamp as a string. */
	jsonTimestamp: boolean;
	/** Validation codes returned for a blank required title. */
	validationCodes: string[];
	/** Whether a failed transaction left no new rows behind. */
	rollbackPreservedCount: boolean;
	/** Rows remaining after deleting the original workspace note. */
	remainingNotes: number;
}

/**
 * Runs the guide against a newly created database and removes it afterwards.
 *
 * Requires a local SQL account allowed to create/drop db3_app_test_* databases.
 * Database.install is deliberately used only inside this disposable lab.
 * Normal applications use reviewed migrations instead of boot-time schema work.
 *
 * @returns Actual database, validation and transport outcomes for the guide.
 */
export async function runWorkspaceNotes(): Promise<WorkspaceNotesOutcome> {
	const database = await createGeneratedTestDatabase('notes_guide');
	const application = new App({ db: database.db });

	try {
		await application.db.install(KnowledgeNote);
		const workspace = '01ARZ3NDEKTSV4RRFFQ69G5FAV';
		const otherWorkspace = '01ARZ3NDEKTSV4RRFFQ69G5FAW';
		const note = await createWorkspaceNote(workspace, { title: '  Client brief  ', body: 'Prepare the proposal.', workspace: otherWorkspace });
		await createWorkspaceNote(otherWorkspace, { title: 'Another client' });
		const notes = await listWorkspaceNotes(workspace);
		const updated = await updateWorkspaceNote(workspace, note.id!, { body: 'Review the brief, then prepare the proposal.' });
		const validationCodes: string[] = [];

		try {
			await createWorkspaceNote(workspace, { title: '   ' });
		} catch (error) {
			if (!(error instanceof RecordValidationError)) throw error;
			validationCodes.push(...error.errors.flatMap(field => field.code ? [field.code] : []));
		}

		const before = await KnowledgeNote.where('workspace', workspace).count();
		try {
			await createNotesTogether(workspace, [{ title: 'This note must roll back' }, { title: '' }]);
		} catch (error) {
			if (!(error instanceof RecordValidationError)) throw error;
		}
		const after = await KnowledgeNote.where('workspace', workspace).count();
		await deleteWorkspaceNote(workspace, note.id!);

		return {
			title: notes[0]?.title ?? null,
			visibleNotes: notes.length,
			workspaceProtected: updated.workspace === workspace,
			jsonTimestamp: typeof updated.toJSON().createdAt === 'string',
			validationCodes,
			rollbackPreservedCount: before === after,
			remainingNotes: await KnowledgeNote.where('workspace', workspace).count(),
		};
	} finally {
		try {
			await application.close();
		} finally {
			await database.destroy();
		}
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
	console.log(JSON.stringify(await runWorkspaceNotes(), null, 2));
}

Try a few changes

Change the note title and run again. Try a blank title or 121 characters: save() should fail with RecordValidationError. Change the update/delete workspace to the other workspace: the scoped lookup should throw RecordNotFoundError.

Run the type check below after editing. It checks your copied source against the installed package declarations.

These checks exercise field protection and database scoping, not your real authentication system. A production endpoint must derive workspaceId from verified membership and recheck the appropriate permission for each operation.

Check the example types
bash
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts

Move it into your application

Keep KnowledgeNote and the feature functions. Use your normal App boot, register the model with your application’s migration manager, and apply reviewed migrations. Do not copy the disposable database creation or Database.install() into request/server startup.

Follow the API and Migrations guides for the next application boundaries. Conflict resolution and reversible soft-delete workflows remain TODO recipes; they are not hidden prerequisites for this lab.

Behaviour tested Executed by the documentation maintenance gate
What this does

Runs the note workflow against a real disposable SQL database, including validation, workspace isolation, updates, deletion and transaction rollback.

Expected output6 tests pass; the walkthrough matches its checked-in JSON output.
Behaviour testpackages/app/src/db/tests/examples/workspaceNotes.test.ts

This test command requires the framework repository. Use the walkthrough commands in an installed application.

Environment: Node.js; MariaDB/MySQL test account with CREATE/DROP DATABASE permission; package-owned test settings. No application database is used.