@skaile/forge-editor-directus 0.1.0 → 0.1.1
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/CHANGELOG.md +10 -0
- package/README.md +4 -0
- package/dist/adapter.js +19 -28
- package/dist/adapter.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Internal: one `upsert` (update, else create) for documents, collaboration states and settings; one content-write record and one id comparator. No behaviour change.
|
|
8
|
+
|
|
9
|
+
### Documented
|
|
10
|
+
|
|
11
|
+
- Directus caps request bodies at 1 MB by default (`MAX_PAYLOAD_SIZE`); hosts with large documents or Yjs states must raise it, otherwise Directus answers 413 and the write throws.
|
|
12
|
+
|
|
3
13
|
## 0.1.0
|
|
4
14
|
|
|
5
15
|
### Added
|
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ CREATE TABLE editor_settings (user_id TEXT PRIMARY KEY NOT NULL, json JSON NOT
|
|
|
31
31
|
|
|
32
32
|
Other names: `collections: { documents, states, settings }`. The token needs full access to the three collections; an admin token works, and the tables need no registration in Directus (untracked tables are reachable through `/items` for admins). Directus 11 answers `GET /items/<c>/<id>` for a missing item with 403, not 404, and `PATCH` of a missing item with 204 and no body, so the adapter never relies on a status to detect a missing item: it reads through `GET /items/<c>?filter[<key>][_eq]=<id>&limit=1` (empty list = missing) and reads a PATCH answer without `data` as "not updated" before it creates the row. A 403 on a list read stays an error (token without access).
|
|
33
33
|
|
|
34
|
+
## Payload limit
|
|
35
|
+
|
|
36
|
+
Directus caps request bodies at 1 MB by default (`MAX_PAYLOAD_SIZE`). The adapter writes a document's content and a collaboration state in one request each, so a host with large documents must raise the limit, e.g. `MAX_PAYLOAD_SIZE=50mb`. Otherwise Directus answers 413 and the write (`storeContent`, `putDocument`, `storeState`) throws.
|
|
37
|
+
|
|
34
38
|
## Transport
|
|
35
39
|
|
|
36
40
|
`restItemsClient({ url, token, fetch? })` implements `DirectusItemsClient` (`readOne` is a filtered list query, empty → `null`; `readMany` with `limit=-1`; `updateOne` PATCH with `fields=<key>`, 204/404/no `data` → `false`; `createOne`; `deleteOne` 204/404 is fine; any other non-2xx throws). Ids are URL-encoded (`gutachten:<uuid>`). Pass `client` instead to use your own transport.
|
package/dist/adapter.js
CHANGED
|
@@ -32,7 +32,19 @@ function createDirectusAdapter(options) {
|
|
|
32
32
|
const documents = options.collections?.documents ?? "editor_documents";
|
|
33
33
|
const states = options.collections?.states ?? "editor_states";
|
|
34
34
|
const settings = options.collections?.settings ?? "editor_settings";
|
|
35
|
-
const
|
|
35
|
+
const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
36
|
+
const contentFields = (content) => ({
|
|
37
|
+
content: JSON.stringify(content),
|
|
38
|
+
version: randomUUID(),
|
|
39
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
40
|
+
});
|
|
41
|
+
/** Update the item, else create it with its key set. */
|
|
42
|
+
const upsert = async (collection, keyColumn, key, fields) => {
|
|
43
|
+
if (!await client.updateOne(collection, key, fields, keyColumn)) await client.createOne(collection, {
|
|
44
|
+
[keyColumn]: key,
|
|
45
|
+
...fields
|
|
46
|
+
});
|
|
47
|
+
};
|
|
36
48
|
return {
|
|
37
49
|
identity: identityOf(options),
|
|
38
50
|
directory: directoryOf(options),
|
|
@@ -41,11 +53,7 @@ function createDirectusAdapter(options) {
|
|
|
41
53
|
return jsonField((await client.readOne(documents, id, ["content"], "id"))?.content);
|
|
42
54
|
},
|
|
43
55
|
async storeContent(id, content) {
|
|
44
|
-
await client.updateOne(documents, id,
|
|
45
|
-
content: JSON.stringify(content),
|
|
46
|
-
version: randomUUID(),
|
|
47
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
48
|
-
}, "id");
|
|
56
|
+
await client.updateOne(documents, id, contentFields(content), "id");
|
|
49
57
|
},
|
|
50
58
|
async contentVersion(id) {
|
|
51
59
|
const version = (await client.readOne(documents, id, ["version"], "id"))?.version;
|
|
@@ -55,25 +63,17 @@ function createDirectusAdapter(options) {
|
|
|
55
63
|
return (await client.readMany(documents, { fields: ["id", "content"] })).map((row) => ({
|
|
56
64
|
id: String(row.id),
|
|
57
65
|
content: jsonField(row.content)
|
|
58
|
-
})).filter((entry) => entry.content !== null).sort(
|
|
66
|
+
})).filter((entry) => entry.content !== null).sort(byId);
|
|
59
67
|
},
|
|
60
68
|
async putDocument(id, content) {
|
|
61
|
-
|
|
62
|
-
content: JSON.stringify(content),
|
|
63
|
-
version: randomUUID(),
|
|
64
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
65
|
-
};
|
|
66
|
-
if (!await client.updateOne(documents, id, fields, "id")) await client.createOne(documents, {
|
|
67
|
-
id,
|
|
68
|
-
...fields
|
|
69
|
-
});
|
|
69
|
+
await upsert(documents, "id", id, contentFields(content));
|
|
70
70
|
},
|
|
71
71
|
async deleteDocument(id) {
|
|
72
72
|
await client.deleteOne(documents, id);
|
|
73
73
|
await client.deleteOne(states, id);
|
|
74
74
|
},
|
|
75
75
|
async listDocumentIds() {
|
|
76
|
-
return
|
|
76
|
+
return (await client.readMany(documents, { fields: ["id"] })).map((row) => ({ id: String(row.id) })).sort(byId).map((row) => row.id);
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
collabState: {
|
|
@@ -82,11 +82,7 @@ function createDirectusAdapter(options) {
|
|
|
82
82
|
return typeof state === "string" ? new Uint8Array(Buffer.from(state, "base64")) : null;
|
|
83
83
|
},
|
|
84
84
|
async storeState(id, state) {
|
|
85
|
-
|
|
86
|
-
if (!await client.updateOne(states, id, { state: encoded }, "id")) await client.createOne(states, {
|
|
87
|
-
id,
|
|
88
|
-
state: encoded
|
|
89
|
-
});
|
|
85
|
+
await upsert(states, "id", id, { state: Buffer.from(state).toString("base64") });
|
|
90
86
|
},
|
|
91
87
|
async removeState(id) {
|
|
92
88
|
await client.deleteOne(states, id);
|
|
@@ -97,12 +93,7 @@ function createDirectusAdapter(options) {
|
|
|
97
93
|
return jsonField((await client.readOne(settings, settingsKey(user), ["json"], "user_id"))?.json);
|
|
98
94
|
},
|
|
99
95
|
async storeSettings(user, value) {
|
|
100
|
-
|
|
101
|
-
const json = JSON.stringify(value);
|
|
102
|
-
if (!await client.updateOne(settings, key, { json }, "user_id")) await client.createOne(settings, {
|
|
103
|
-
user_id: key,
|
|
104
|
-
json
|
|
105
|
-
});
|
|
96
|
+
await upsert(settings, "user_id", settingsKey(user), { json: JSON.stringify(value) });
|
|
106
97
|
}
|
|
107
98
|
}
|
|
108
99
|
};
|
package/dist/adapter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","names":[],"sources":["../src/adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { AdapterHooks, StoredPorts } from '@skaile/forge-editor/adapters'\nimport type { JSONContent } from '@tiptap/core'\nimport { type DirectusItemsClient,
|
|
1
|
+
{"version":3,"file":"adapter.js","names":[],"sources":["../src/adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { AdapterHooks, StoredPorts } from '@skaile/forge-editor/adapters'\nimport type { JSONContent } from '@tiptap/core'\nimport { type DirectusItemsClient, restItemsClient } from './client'\nimport { directoryOf, identityOf, settingsKey } from './hooks'\n\nexport interface DirectusAdapterOptions extends AdapterHooks {\n /** Base URL of the Directus instance. */\n url: string\n /** Static token with access to the three collections (an admin token, as the app uses today). */\n token: string\n /** Collection names; defaults `editor_documents`, `editor_states`, `editor_settings`. */\n collections?: { documents?: string, states?: string, settings?: string }\n /** The transport (tests); default `restItemsClient({ url, token, fetch })`. */\n client?: DirectusItemsClient\n fetch?: typeof fetch\n}\n\n/** The stored ports of an editor host, without `assets` (the image routes answer 501). */\nexport type DirectusPorts = Omit<StoredPorts, 'assets'>\n\n/**\n * Directus returns a `json` field parsed when the collection is registered with the `cast-json` special and as the stored\n * text otherwise; the adapter writes JSON as text (valid either way) and reads both.\n */\nfunction jsonField<T>(value: unknown): T | null {\n if (value === null || value === undefined) return null\n return (typeof value === 'string' ? JSON.parse(value) : value) as T\n}\n\n/**\n * The editor's adapter on three Directus collections over the REST items API:\n *\n * editor_documents(id PK, content json, version text, updated_at text) TipTap JSON + opaque content version\n * editor_states(id PK, state text) the collab server's state bytes, base64\n * editor_settings(user_id PK, json json) per-user editor settings\n *\n * Every write of a document's content (`storeContent`, `putDocument`) stores a fresh random `version`, which\n * `contentVersion` returns. `storeContent` only updates existing documents: create them with `putDocument`. There is\n * no `listTree` / `createDocument` / `moveDocument` (the document routes answer 501), no `assets` port and no theme\n * library. Single-item requests only, no transaction: `deleteDocument` removes the content first, so a concurrent\n * `storeContent` finds nothing to update.\n */\nexport function createDirectusAdapter(options: DirectusAdapterOptions): DirectusPorts {\n const client = options.client ?? restItemsClient({ url: options.url, token: options.token, fetch: options.fetch })\n const documents = options.collections?.documents ?? 'editor_documents'\n const states = options.collections?.states ?? 'editor_states'\n const settings = options.collections?.settings ?? 'editor_settings'\n\n const byId = (a: { id: string }, b: { id: string }) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0\n const contentFields = (content: JSONContent) => ({ content: JSON.stringify(content), version: randomUUID(), updated_at: new Date().toISOString() })\n /** Update the item, else create it with its key set. */\n const upsert = async (collection: string, keyColumn: string, key: string, fields: Record<string, unknown>) => {\n if (!await client.updateOne(collection, key, fields, keyColumn)) await client.createOne(collection, { [keyColumn]: key, ...fields })\n }\n\n return {\n identity: identityOf(options),\n directory: directoryOf(options),\n\n documents: {\n async loadContent(id) {\n return jsonField<JSONContent>((await client.readOne(documents, id, ['content'], 'id'))?.content)\n },\n async storeContent(id, content) {\n await client.updateOne(documents, id, contentFields(content), 'id')\n },\n async contentVersion(id) {\n const version = (await client.readOne(documents, id, ['version'], 'id'))?.version\n return typeof version === 'string' ? version : null\n },\n async listContents() {\n const rows = await client.readMany(documents, { fields: ['id', 'content'] })\n return rows\n .map(row => ({ id: String(row.id), content: jsonField<JSONContent>(row.content) }))\n .filter((entry): entry is { id: string, content: JSONContent } => entry.content !== null)\n .sort(byId)\n },\n\n async putDocument(id, content) {\n await upsert(documents, 'id', id, contentFields(content))\n },\n async deleteDocument(id) {\n await client.deleteOne(documents, id)\n await client.deleteOne(states, id)\n },\n async listDocumentIds() {\n return (await client.readMany(documents, { fields: ['id'] })).map(row => ({ id: String(row.id) })).sort(byId).map(row => row.id)\n },\n },\n\n collabState: {\n async loadState(id) {\n const state = (await client.readOne(states, id, ['state'], 'id'))?.state\n return typeof state === 'string' ? new Uint8Array(Buffer.from(state, 'base64')) : null\n },\n async storeState(id, state) {\n await upsert(states, 'id', id, { state: Buffer.from(state).toString('base64') })\n },\n async removeState(id) {\n await client.deleteOne(states, id)\n },\n },\n\n settings: {\n async loadSettings(user) {\n return jsonField((await client.readOne(settings, settingsKey(user), ['json'], 'user_id'))?.json)\n },\n async storeSettings(user, value) {\n await upsert(settings, 'user_id', settingsKey(user), { json: JSON.stringify(value) })\n },\n },\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/forge-editor-directus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Directus storage for @skaile/forge-editor: documents, collaboration state and per-user settings over the Directus REST items API (fetch only, no Directus SDK).",
|
|
6
6
|
"license": "MIT",
|