Add Rails backend: accounts via magic link, server documents, sharing
The app becomes a Rails 8 application; the Vue frontend moves to app/frontend and is served through vite_rails. - Without an account documents stay in the browser (IndexedDB). Sign-in is by magic link or the 6-digit code from the same mail; afterwards local documents can be uploaded into the account. - Server documents (/d/<id>) with versioned saves and a single-editor lock per browser tab; others see them read-only and follow by polling. - Sharing via link (works without an account) and via e-mail invitation, roles viewer/editor; the owner manages links and people. - Store gains a read-only mode that blocks all content changes. - Solid Queue, Cache and Cable share the one PostgreSQL database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { IDBFactory } from 'fake-indexeddb';
|
||||
import { DocumentLibrary, newDocId, type DocMeta } from '../library';
|
||||
|
||||
const meta = (id: string, title: string, updatedAt: number): DocMeta => ({
|
||||
id,
|
||||
title,
|
||||
createdAt: 1,
|
||||
updatedAt,
|
||||
});
|
||||
|
||||
/** a fresh, empty in-memory IndexedDB per test */
|
||||
const openLib = () => DocumentLibrary.open(new IDBFactory());
|
||||
|
||||
describe('DocumentLibrary', () => {
|
||||
it('stores meta and content separately and reads them back', async () => {
|
||||
const lib = await openLib();
|
||||
await lib.put(meta('a', 'Budget', 10), '{"x":1}');
|
||||
expect(await lib.getMeta('a')).toEqual(meta('a', 'Budget', 10));
|
||||
expect(await lib.getContent('a')).toBe('{"x":1}');
|
||||
expect(await lib.getContent('missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('lists documents most recently changed first', async () => {
|
||||
const lib = await openLib();
|
||||
await lib.put(meta('old', 'Alt', 10), '{}');
|
||||
await lib.put(meta('new', 'Neu', 30), '{}');
|
||||
await lib.put(meta('mid', 'Mitte', 20), '{}');
|
||||
expect((await lib.list()).map((d) => d.id)).toEqual(['new', 'mid', 'old']);
|
||||
});
|
||||
|
||||
it('patchMeta renames without touching content; unknown ids are ignored', async () => {
|
||||
const lib = await openLib();
|
||||
await lib.put(meta('a', 'Alt', 10), '{"keep":true}');
|
||||
await lib.patchMeta('a', { title: 'Neu' });
|
||||
await lib.patchMeta('nope', { title: 'X' });
|
||||
expect((await lib.getMeta('a'))?.title).toBe('Neu');
|
||||
expect(await lib.getContent('a')).toBe('{"keep":true}');
|
||||
expect(await lib.list()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('remove deletes meta and content', async () => {
|
||||
const lib = await openLib();
|
||||
await lib.put(meta('a', 'A', 10), '{}');
|
||||
await lib.remove('a');
|
||||
expect(await lib.getMeta('a')).toBeUndefined();
|
||||
expect(await lib.getContent('a')).toBeUndefined();
|
||||
expect(await lib.list()).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps data across re-opening the same database', async () => {
|
||||
const factory = new IDBFactory();
|
||||
await (await DocumentLibrary.open(factory)).put(meta('a', 'A', 10), '{"v":2}');
|
||||
expect(await (await DocumentLibrary.open(factory)).getContent('a')).toBe('{"v":2}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newDocId', () => {
|
||||
it('produces distinct v4 UUIDs', () => {
|
||||
const a = newDocId();
|
||||
expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
expect(newDocId()).not.toBe(a);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user