storybooker 0.19.4 → 0.22.0-canary.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.
- package/README.md +40 -18
- package/dist/adapters/_internal/queue.d.mts +127 -0
- package/dist/aws-dynamodb.d.mts +22 -0
- package/dist/aws-dynamodb.mjs +118 -0
- package/dist/aws-dynamodb.mjs.map +1 -0
- package/dist/aws-s3.d.mts +20 -0
- package/dist/aws-s3.mjs +96 -0
- package/dist/aws-s3.mjs.map +1 -0
- package/dist/azure-blob-storage.d.mts +20 -0
- package/dist/azure-blob-storage.mjs +126 -0
- package/dist/azure-blob-storage.mjs.map +1 -0
- package/dist/azure-cosmos-db.d.mts +23 -0
- package/dist/azure-cosmos-db.mjs +87 -0
- package/dist/azure-cosmos-db.mjs.map +1 -0
- package/dist/azure-data-tables.d.mts +23 -0
- package/dist/azure-data-tables.mjs +127 -0
- package/dist/azure-data-tables.mjs.map +1 -0
- package/dist/azure-easy-auth.d.mts +50 -0
- package/dist/azure-easy-auth.mjs +88 -0
- package/dist/azure-easy-auth.mjs.map +1 -0
- package/dist/azure-functions.d.mts +62 -0
- package/dist/azure-functions.mjs +147 -0
- package/dist/azure-functions.mjs.map +1 -0
- package/dist/fs.d.mts +37 -0
- package/dist/fs.mjs +240 -0
- package/dist/fs.mjs.map +1 -0
- package/dist/gcp-big-table.d.mts +23 -0
- package/dist/gcp-big-table.mjs +92 -0
- package/dist/gcp-big-table.mjs.map +1 -0
- package/dist/gcp-firestore.d.mts +22 -0
- package/dist/gcp-firestore.mjs +87 -0
- package/dist/gcp-firestore.mjs.map +1 -0
- package/dist/gcp-storage.d.mts +20 -0
- package/dist/gcp-storage.mjs +96 -0
- package/dist/gcp-storage.mjs.map +1 -0
- package/dist/handlers/handle-process-zip.mjs +90 -0
- package/dist/handlers/handle-process-zip.mjs.map +1 -0
- package/dist/handlers/handle-purge.d.mts +12 -0
- package/dist/handlers/handle-purge.mjs +36 -0
- package/dist/handlers/handle-purge.mjs.map +1 -0
- package/dist/handlers/handle-serve-storybook.mjs +94 -0
- package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +62 -0
- package/dist/index.mjs.map +1 -0
- package/dist/models/builds-model.mjs +248 -0
- package/dist/models/builds-model.mjs.map +1 -0
- package/dist/models/builds-schema.d.mts +171 -0
- package/dist/models/builds-schema.mjs +67 -0
- package/dist/models/builds-schema.mjs.map +1 -0
- package/dist/models/projects-model.mjs +122 -0
- package/dist/models/projects-model.mjs.map +1 -0
- package/dist/models/projects-schema.d.mts +70 -0
- package/dist/models/projects-schema.mjs +37 -0
- package/dist/models/projects-schema.mjs.map +1 -0
- package/dist/models/tags-model.mjs +110 -0
- package/dist/models/tags-model.mjs.map +1 -0
- package/dist/models/tags-schema.d.mts +76 -0
- package/dist/models/tags-schema.mjs +34 -0
- package/dist/models/tags-schema.mjs.map +1 -0
- package/dist/models/~model.mjs +43 -0
- package/dist/models/~model.mjs.map +1 -0
- package/dist/models/~shared-schema.d.mts +1 -0
- package/dist/models/~shared-schema.mjs +20 -0
- package/dist/models/~shared-schema.mjs.map +1 -0
- package/dist/mysql.d.mts +39 -0
- package/dist/mysql.mjs +151 -0
- package/dist/mysql.mjs.map +1 -0
- package/dist/redis.d.mts +33 -0
- package/dist/redis.mjs +118 -0
- package/dist/redis.mjs.map +1 -0
- package/dist/routers/account-router.mjs +91 -0
- package/dist/routers/account-router.mjs.map +1 -0
- package/dist/routers/builds-router.mjs +347 -0
- package/dist/routers/builds-router.mjs.map +1 -0
- package/dist/routers/projects-router.mjs +236 -0
- package/dist/routers/projects-router.mjs.map +1 -0
- package/dist/routers/root-router.mjs +108 -0
- package/dist/routers/root-router.mjs.map +1 -0
- package/dist/routers/tags-router.mjs +269 -0
- package/dist/routers/tags-router.mjs.map +1 -0
- package/dist/routers/tasks-router.mjs +71 -0
- package/dist/routers/tasks-router.mjs.map +1 -0
- package/dist/urls.d.mts +47 -0
- package/dist/urls.mjs +208 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/adapter-utils.d.mts +14 -0
- package/dist/utils/adapter-utils.mjs +14 -0
- package/dist/utils/adapter-utils.mjs.map +1 -0
- package/dist/utils/auth.mjs +25 -0
- package/dist/utils/auth.mjs.map +1 -0
- package/dist/utils/error.d.mts +21 -0
- package/dist/utils/error.mjs +109 -0
- package/dist/utils/error.mjs.map +1 -0
- package/dist/utils/file-utils.mjs +16 -0
- package/dist/utils/file-utils.mjs.map +1 -0
- package/dist/utils/openapi-utils.mjs +45 -0
- package/dist/utils/openapi-utils.mjs.map +1 -0
- package/dist/utils/request.mjs +35 -0
- package/dist/utils/request.mjs.map +1 -0
- package/dist/utils/response.mjs +24 -0
- package/dist/utils/response.mjs.map +1 -0
- package/dist/utils/store.mjs +54 -0
- package/dist/utils/store.mjs.map +1 -0
- package/dist/utils/ui-utils.mjs +38 -0
- package/dist/utils/ui-utils.mjs.map +1 -0
- package/dist/utils/url-utils.d.mts +10 -0
- package/dist/utils/url-utils.mjs +54 -0
- package/dist/utils/url-utils.mjs.map +1 -0
- package/dist/~internal/adapter/auth.d.mts +123 -0
- package/dist/~internal/adapter/auth.mjs +20 -0
- package/dist/~internal/adapter/auth.mjs.map +1 -0
- package/dist/~internal/adapter/database.d.mts +240 -0
- package/dist/~internal/adapter/database.mjs +63 -0
- package/dist/~internal/adapter/database.mjs.map +1 -0
- package/dist/~internal/adapter/logger.d.mts +34 -0
- package/dist/~internal/adapter/logger.mjs +13 -0
- package/dist/~internal/adapter/logger.mjs.map +1 -0
- package/dist/~internal/adapter/storage.d.mts +208 -0
- package/dist/~internal/adapter/storage.mjs +63 -0
- package/dist/~internal/adapter/storage.mjs.map +1 -0
- package/dist/~internal/adapter/ui.d.mts +109 -0
- package/dist/~internal/adapter/ui.mjs +1 -0
- package/dist/~internal/adapter.d.mts +8 -0
- package/dist/~internal/adapter.mjs +6 -0
- package/dist/~internal/constants.d.mts +24 -0
- package/dist/~internal/constants.mjs +32 -0
- package/dist/~internal/constants.mjs.map +1 -0
- package/dist/~internal/mimes.d.mts +449 -0
- package/dist/~internal/mimes.mjs +454 -0
- package/dist/~internal/mimes.mjs.map +1 -0
- package/dist/~internal/router.d.mts +1651 -0
- package/dist/~internal/router.mjs +39 -0
- package/dist/~internal/router.mjs.map +1 -0
- package/dist/~internal/types.d.mts +77 -0
- package/dist/~internal/types.mjs +1 -0
- package/dist/~internal/utils.d.mts +4 -0
- package/dist/~internal/utils.mjs +5 -0
- package/openapi.json +3162 -0
- package/package.json +148 -27
- package/src/adapters/_internal/auth.ts +135 -0
- package/src/adapters/_internal/database.ts +241 -0
- package/src/adapters/_internal/index.ts +8 -0
- package/src/adapters/_internal/logger.ts +41 -0
- package/src/adapters/_internal/queue.ts +151 -0
- package/src/adapters/_internal/storage.ts +197 -0
- package/src/adapters/_internal/ui.ts +103 -0
- package/src/adapters/aws-dynamodb.ts +201 -0
- package/src/adapters/aws-s3.ts +160 -0
- package/src/adapters/azure-blob-storage.ts +223 -0
- package/src/adapters/azure-cosmos-db.ts +158 -0
- package/src/adapters/azure-data-tables.ts +223 -0
- package/src/adapters/azure-easy-auth.ts +174 -0
- package/src/adapters/azure-functions.ts +242 -0
- package/src/adapters/fs.ts +398 -0
- package/src/adapters/gcp-big-table.ts +157 -0
- package/src/adapters/gcp-firestore.ts +146 -0
- package/src/adapters/gcp-storage.ts +141 -0
- package/src/adapters/mysql.ts +296 -0
- package/src/adapters/redis.ts +242 -0
- package/src/handlers/handle-process-zip.ts +117 -0
- package/src/handlers/handle-purge.ts +65 -0
- package/src/handlers/handle-serve-storybook.ts +101 -0
- package/src/index.ts +81 -16
- package/src/mocks/mock-auth-service.ts +51 -0
- package/src/mocks/mock-store.ts +26 -0
- package/src/models/builds-model.ts +373 -0
- package/src/models/builds-schema.ts +84 -0
- package/src/models/projects-model.ts +177 -0
- package/src/models/projects-schema.ts +69 -0
- package/src/models/tags-model.ts +138 -0
- package/src/models/tags-schema.ts +45 -0
- package/src/models/~model.ts +79 -0
- package/src/models/~shared-schema.ts +14 -0
- package/src/routers/_app-router.ts +57 -0
- package/src/routers/account-router.ts +136 -0
- package/src/routers/builds-router.ts +464 -0
- package/src/routers/projects-router.ts +309 -0
- package/src/routers/root-router.ts +127 -0
- package/src/routers/tags-router.ts +339 -0
- package/src/routers/tasks-router.ts +75 -0
- package/src/types.ts +107 -0
- package/src/urls.ts +327 -0
- package/src/utils/adapter-utils.ts +26 -0
- package/src/utils/auth.test.ts +71 -0
- package/src/utils/auth.ts +39 -0
- package/src/utils/constants.ts +31 -0
- package/src/utils/date-utils.ts +10 -0
- package/src/utils/error.test.ts +86 -0
- package/src/utils/error.ts +140 -0
- package/src/utils/file-utils.test.ts +65 -0
- package/src/utils/file-utils.ts +43 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/mime-utils.ts +457 -0
- package/src/utils/openapi-utils.ts +49 -0
- package/src/utils/request.ts +97 -0
- package/src/utils/response.ts +20 -0
- package/src/utils/store.ts +85 -0
- package/src/utils/story-utils.ts +42 -0
- package/src/utils/text-utils.ts +10 -0
- package/src/utils/ui-utils.ts +57 -0
- package/src/utils/url-utils.ts +113 -0
- package/dist/index.js +0 -554
- package/src/commands/create.ts +0 -263
- package/src/commands/purge.ts +0 -70
- package/src/commands/test.ts +0 -42
- package/src/service-schema.d.ts +0 -2023
- package/src/utils/auth-utils.ts +0 -31
- package/src/utils/pkg-utils.ts +0 -37
- package/src/utils/sb-build.ts +0 -55
- package/src/utils/sb-test.ts +0 -115
- package/src/utils/schema-utils.ts +0 -123
- package/src/utils/stream-utils.ts +0 -72
- package/src/utils/types.ts +0 -4
- package/src/utils/zip.ts +0 -77
package/dist/mysql.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { DatabaseAdapterErrors } from "./~internal/adapter/database.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/mysql.ts
|
|
4
|
+
/**
|
|
5
|
+
* MySQL database adapter for StoryBooker.
|
|
6
|
+
* Uses tables to represent collections and rows to represent documents.
|
|
7
|
+
*
|
|
8
|
+
* Table structure:
|
|
9
|
+
* - Each collection becomes a table with name: `sb_{collectionId}`
|
|
10
|
+
* - Each table has columns: id (VARCHAR PRIMARY KEY), data (JSON), created_at (TIMESTAMP), updated_at (TIMESTAMP)
|
|
11
|
+
* - Collections metadata stored in `sb_collections` table
|
|
12
|
+
*
|
|
13
|
+
* Compatible with mysql2, mysql, and other MySQL libraries that implement the connection interface.
|
|
14
|
+
*/
|
|
15
|
+
var MySQLDatabaseAdapter = class {
|
|
16
|
+
#connection;
|
|
17
|
+
#tablePrefix;
|
|
18
|
+
constructor(connection, tablePrefix = "sb") {
|
|
19
|
+
this.#connection = connection;
|
|
20
|
+
this.#tablePrefix = tablePrefix;
|
|
21
|
+
}
|
|
22
|
+
metadata = { name: "MySQL" };
|
|
23
|
+
init = async (_options) => {
|
|
24
|
+
try {
|
|
25
|
+
const collectionsTable = `${this.#tablePrefix}_collections`;
|
|
26
|
+
await this.#connection.connect?.();
|
|
27
|
+
await this.#connection.execute(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS \`${collectionsTable}\` (
|
|
29
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
30
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
32
|
+
`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
#getTableName(collectionId) {
|
|
38
|
+
return `${this.#tablePrefix}_${collectionId}`;
|
|
39
|
+
}
|
|
40
|
+
#getCollectionsTableName() {
|
|
41
|
+
return `${this.#tablePrefix}_collections`;
|
|
42
|
+
}
|
|
43
|
+
#formatDocumentRow(row) {
|
|
44
|
+
if ("data" in row && typeof row["data"] === "string") return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
...JSON.parse(row["data"])
|
|
47
|
+
};
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
listCollections = async (_options) => {
|
|
51
|
+
const collectionsTable = this.#getCollectionsTableName();
|
|
52
|
+
const [rows] = await this.#connection.execute(`SELECT id FROM \`${collectionsTable}\``);
|
|
53
|
+
return rows.map((row) => row.id);
|
|
54
|
+
};
|
|
55
|
+
createCollection = async (collectionId, _options) => {
|
|
56
|
+
try {
|
|
57
|
+
const tableName = this.#getTableName(collectionId);
|
|
58
|
+
const collectionsTable = this.#getCollectionsTableName();
|
|
59
|
+
await this.#connection.execute(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
|
61
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
62
|
+
data JSON NOT NULL,
|
|
63
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
64
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
65
|
+
INDEX idx_created_at (created_at)
|
|
66
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
67
|
+
`);
|
|
68
|
+
await this.#connection.execute(`INSERT IGNORE INTO \`${collectionsTable}\` (id) VALUES (?)`, [collectionId]);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
hasCollection = async (collectionId, _options) => {
|
|
74
|
+
const collectionsTable = this.#getCollectionsTableName();
|
|
75
|
+
const [rows] = await this.#connection.execute(`SELECT 1 FROM \`${collectionsTable}\` WHERE id = ? LIMIT 1`, [collectionId]);
|
|
76
|
+
return rows.length > 0;
|
|
77
|
+
};
|
|
78
|
+
deleteCollection = async (collectionId, _options) => {
|
|
79
|
+
const tableName = this.#getTableName(collectionId);
|
|
80
|
+
const collectionsTable = this.#getCollectionsTableName();
|
|
81
|
+
try {
|
|
82
|
+
await this.#connection.execute(`DROP TABLE IF EXISTS \`${tableName}\``);
|
|
83
|
+
await this.#connection.execute(`DELETE FROM \`${collectionsTable}\` WHERE id = ?`, [collectionId]);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
listDocuments = async (collectionId, listOptions, _options) => {
|
|
89
|
+
let query = `SELECT id, data FROM \`${this.#getTableName(collectionId)}\``;
|
|
90
|
+
const params = [];
|
|
91
|
+
if (listOptions?.filter && typeof listOptions.filter === "string") query += ` WHERE ${listOptions.filter}`;
|
|
92
|
+
if (listOptions?.sort && listOptions.sort === "latest") query += ` ORDER BY created_at DESC`;
|
|
93
|
+
if (listOptions?.limit && listOptions.limit > 0) {
|
|
94
|
+
query += ` LIMIT ?`;
|
|
95
|
+
params.push(listOptions.limit);
|
|
96
|
+
}
|
|
97
|
+
const [rows] = await this.#connection.execute(query, params);
|
|
98
|
+
let documents = [];
|
|
99
|
+
for (const row of rows) documents.push(this.#formatDocumentRow(row));
|
|
100
|
+
const filterFn = listOptions?.filter && typeof listOptions.filter === "function" ? listOptions.filter : void 0;
|
|
101
|
+
if (filterFn) documents = documents.filter((item) => filterFn(item));
|
|
102
|
+
if (listOptions?.sort && typeof listOptions.sort === "function") documents.sort(listOptions.sort);
|
|
103
|
+
if (listOptions?.select && listOptions.select.length > 0) documents = documents.map((doc) => {
|
|
104
|
+
const projected = { id: doc.id };
|
|
105
|
+
for (const field of listOptions.select) if (field in doc) projected[field] = doc[field];
|
|
106
|
+
return projected;
|
|
107
|
+
});
|
|
108
|
+
return documents;
|
|
109
|
+
};
|
|
110
|
+
getDocument = async (collectionId, documentId, _options) => {
|
|
111
|
+
const tableName = this.#getTableName(collectionId);
|
|
112
|
+
const [rows] = await this.#connection.execute(`SELECT id, data FROM \`${tableName}\` WHERE id = ? LIMIT 1`, [documentId]);
|
|
113
|
+
const [row] = rows;
|
|
114
|
+
if (!row) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
115
|
+
return this.#formatDocumentRow(row);
|
|
116
|
+
};
|
|
117
|
+
hasDocument = async (collectionId, documentId, _options) => {
|
|
118
|
+
const tableName = this.#getTableName(collectionId);
|
|
119
|
+
const [rows] = await this.#connection.execute(`SELECT 1 FROM \`${tableName}\` WHERE id = ? LIMIT 1`, [documentId]);
|
|
120
|
+
return rows.length > 0;
|
|
121
|
+
};
|
|
122
|
+
createDocument = async (collectionId, documentData, _options) => {
|
|
123
|
+
const tableName = this.#getTableName(collectionId);
|
|
124
|
+
const { id, ...data } = documentData;
|
|
125
|
+
try {
|
|
126
|
+
await this.#connection.execute(`INSERT INTO \`${tableName}\` (id, data) VALUES (?, ?)`, [id, JSON.stringify(data)]);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(collectionId, documentData.id, error);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
updateDocument = async (collectionId, documentId, documentData) => {
|
|
132
|
+
const tableName = this.#getTableName(collectionId);
|
|
133
|
+
const [rows] = await this.#connection.execute(`SELECT data FROM \`${tableName}\` WHERE id = ? LIMIT 1`, [documentId]);
|
|
134
|
+
const [row] = rows;
|
|
135
|
+
if (!row) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
136
|
+
const updatedData = {
|
|
137
|
+
..."data" in row && typeof row["data"] === "string" ? JSON.parse(row["data"]) : null,
|
|
138
|
+
...documentData
|
|
139
|
+
};
|
|
140
|
+
await this.#connection.execute(`UPDATE \`${tableName}\` SET data = ? WHERE id = ?`, [JSON.stringify(updatedData), documentId]);
|
|
141
|
+
};
|
|
142
|
+
deleteDocument = async (collectionId, documentId, _options) => {
|
|
143
|
+
const tableName = this.#getTableName(collectionId);
|
|
144
|
+
const [result] = await this.#connection.execute(`DELETE FROM \`${tableName}\` WHERE id = ?`, [documentId]);
|
|
145
|
+
if (result.affectedRows === 0) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
export { MySQLDatabaseAdapter };
|
|
151
|
+
//# sourceMappingURL=mysql.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.mjs","names":["#connection","#tablePrefix","#getCollectionsTableName","#getTableName","params: unknown[]","documents: Document[]","#formatDocumentRow"],"sources":["../src/adapters/mysql.ts"],"sourcesContent":["// oxlint-disable max-lines-per-function\n// oxlint-disable no-unsafe-member-access\n\nimport {\n DatabaseAdapterErrors,\n type DatabaseAdapter,\n type DatabaseAdapterOptions,\n type DatabaseDocumentListOptions,\n type StoryBookerDatabaseDocument,\n} from \"./_internal/database.ts\";\n\n// Define a generic SQL connection interface that works with multiple MySQL libraries\nexport interface MySQLConnection {\n connect?(): Promise<void>;\n execute<Data>(query: string, params?: unknown[]): Promise<[Data]>;\n query<Data>(query: string, params?: unknown[]): Promise<[Data]>;\n end?(): Promise<void>;\n}\n\n/**\n * MySQL database adapter for StoryBooker.\n * Uses tables to represent collections and rows to represent documents.\n *\n * Table structure:\n * - Each collection becomes a table with name: `sb_{collectionId}`\n * - Each table has columns: id (VARCHAR PRIMARY KEY), data (JSON), created_at (TIMESTAMP), updated_at (TIMESTAMP)\n * - Collections metadata stored in `sb_collections` table\n *\n * Compatible with mysql2, mysql, and other MySQL libraries that implement the connection interface.\n */\nexport class MySQLDatabaseAdapter implements DatabaseAdapter {\n #connection: MySQLConnection;\n #tablePrefix: string;\n\n constructor(connection: MySQLConnection, tablePrefix = \"sb\") {\n this.#connection = connection;\n this.#tablePrefix = tablePrefix;\n }\n\n metadata: DatabaseAdapter[\"metadata\"] = { name: \"MySQL\" };\n\n init: DatabaseAdapter[\"init\"] = async (_options) => {\n // Create collections metadata table\n try {\n const collectionsTable = `${this.#tablePrefix}_collections`;\n await this.#connection.connect?.();\n await this.#connection.execute(`\n CREATE TABLE IF NOT EXISTS \\`${collectionsTable}\\` (\n id VARCHAR(255) PRIMARY KEY,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\n `);\n } catch (error) {\n throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);\n }\n };\n\n // Helper methods\n #getTableName(collectionId: string): string {\n return `${this.#tablePrefix}_${collectionId}`;\n }\n\n #getCollectionsTableName(): string {\n return `${this.#tablePrefix}_collections`;\n }\n\n // oxlint-disable-next-line class-methods-use-this\n #formatDocumentRow<Document extends StoryBookerDatabaseDocument>(row: { id: string }): Document {\n if (\"data\" in row && typeof row[\"data\"] === \"string\") {\n return { id: row.id, ...JSON.parse(row[\"data\"]) } as Document;\n }\n return row as unknown as Document;\n }\n\n listCollections: DatabaseAdapter[\"listCollections\"] = async (_options) => {\n const collectionsTable = this.#getCollectionsTableName();\n const [rows] = await this.#connection.execute<StoryBookerDatabaseDocument[]>(\n `SELECT id FROM \\`${collectionsTable}\\``,\n );\n return rows.map((row) => row.id);\n };\n\n createCollection: DatabaseAdapter[\"createCollection\"] = async (collectionId, _options) => {\n try {\n const tableName = this.#getTableName(collectionId);\n const collectionsTable = this.#getCollectionsTableName();\n\n // Create the collection table\n await this.#connection.execute(`\n CREATE TABLE IF NOT EXISTS \\`${tableName}\\` (\n id VARCHAR(255) PRIMARY KEY,\n data JSON NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n INDEX idx_created_at (created_at)\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\n `);\n\n // Register collection\n await this.#connection.execute(`INSERT IGNORE INTO \\`${collectionsTable}\\` (id) VALUES (?)`, [\n collectionId,\n ]);\n } catch (error) {\n throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);\n }\n };\n\n hasCollection: DatabaseAdapter[\"hasCollection\"] = async (collectionId, _options) => {\n const collectionsTable = this.#getCollectionsTableName();\n const [rows] = await this.#connection.execute<StoryBookerDatabaseDocument[]>(\n `SELECT 1 FROM \\`${collectionsTable}\\` WHERE id = ? LIMIT 1`,\n [collectionId],\n );\n return rows.length > 0;\n };\n\n deleteCollection: DatabaseAdapter[\"deleteCollection\"] = async (collectionId, _options) => {\n const tableName = this.#getTableName(collectionId);\n const collectionsTable = this.#getCollectionsTableName();\n\n try {\n // Drop the table\n await this.#connection.execute(`DROP TABLE IF EXISTS \\`${tableName}\\``);\n\n // Remove from collections registry\n await this.#connection.execute(`DELETE FROM \\`${collectionsTable}\\` WHERE id = ?`, [\n collectionId,\n ]);\n } catch (error) {\n throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);\n }\n };\n\n listDocuments: DatabaseAdapter[\"listDocuments\"] = async <\n Document extends StoryBookerDatabaseDocument,\n >(\n collectionId: string,\n listOptions: DatabaseDocumentListOptions<Document>,\n _options: DatabaseAdapterOptions,\n ): Promise<Document[]> => {\n const tableName = this.#getTableName(collectionId);\n\n let query = `SELECT id, data FROM \\`${tableName}\\``;\n const params: unknown[] = [];\n\n // Apply string-based filtering (basic WHERE clause support)\n if (listOptions?.filter && typeof listOptions.filter === \"string\") {\n query += ` WHERE ${listOptions.filter}`;\n }\n\n // Apply sorting\n if (listOptions?.sort && listOptions.sort === \"latest\") {\n query += ` ORDER BY created_at DESC`;\n\n // Function-based sorting will be applied in memory\n }\n\n // Apply limit\n if (listOptions?.limit && listOptions.limit > 0) {\n query += ` LIMIT ?`;\n params.push(listOptions.limit);\n }\n\n const [rows] = await this.#connection.execute<Document[]>(query, params);\n let documents: Document[] = [];\n for (const row of rows) {\n documents.push(this.#formatDocumentRow<Document>(row));\n }\n\n // Apply function-based filtering\n const filterFn =\n listOptions?.filter && typeof listOptions.filter === \"function\"\n ? listOptions.filter\n : undefined;\n if (filterFn) {\n documents = documents.filter((item) => filterFn(item));\n }\n\n // Apply function-based sorting\n if (listOptions?.sort && typeof listOptions.sort === \"function\") {\n documents.sort(listOptions.sort);\n }\n\n // Apply field selection (projection)\n if (listOptions?.select && listOptions.select.length > 0) {\n documents = documents.map((doc) => {\n const projected = { id: doc.id } as Document;\n // oxlint-disable-next-line no-non-null-assertion\n for (const field of listOptions.select!) {\n if (field in doc) {\n // oxlint-disable-next-line no-explicit-any\n (projected as any)[field] = (doc as any)[field];\n }\n }\n return projected;\n });\n }\n\n return documents;\n };\n\n getDocument: DatabaseAdapter[\"getDocument\"] = async <\n Document extends StoryBookerDatabaseDocument,\n >(\n collectionId: string,\n documentId: string,\n _options: DatabaseAdapterOptions,\n ): Promise<Document> => {\n const tableName = this.#getTableName(collectionId);\n const [rows] = await this.#connection.execute<Document[]>(\n `SELECT id, data FROM \\`${tableName}\\` WHERE id = ? LIMIT 1`,\n [documentId],\n );\n\n const [row] = rows;\n if (!row) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n\n return this.#formatDocumentRow<Document>(row);\n };\n\n hasDocument: DatabaseAdapter[\"hasDocument\"] = async (collectionId, documentId, _options) => {\n const tableName = this.#getTableName(collectionId);\n const [rows] = await this.#connection.execute<Document[]>(\n `SELECT 1 FROM \\`${tableName}\\` WHERE id = ? LIMIT 1`,\n [documentId],\n );\n return rows.length > 0;\n };\n\n createDocument: DatabaseAdapter[\"createDocument\"] = async (\n collectionId,\n documentData,\n _options,\n ) => {\n const tableName = this.#getTableName(collectionId);\n const { id, ...data } = documentData;\n try {\n await this.#connection.execute(`INSERT INTO \\`${tableName}\\` (id, data) VALUES (?, ?)`, [\n id,\n JSON.stringify(data),\n ]);\n } catch (error) {\n throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(\n collectionId,\n documentData.id,\n error,\n );\n }\n };\n\n updateDocument: DatabaseAdapter[\"updateDocument\"] = async (\n collectionId,\n documentId,\n documentData,\n ) => {\n const tableName = this.#getTableName(collectionId);\n\n // Get existing document\n const [rows] = await this.#connection.execute<StoryBookerDatabaseDocument[]>(\n `SELECT data FROM \\`${tableName}\\` WHERE id = ? LIMIT 1`,\n [documentId],\n );\n\n const [row] = rows;\n if (!row) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n\n const existingData =\n \"data\" in row && typeof row[\"data\"] === \"string\" ? JSON.parse(row[\"data\"]) : null;\n const updatedData = { ...existingData, ...documentData };\n\n await this.#connection.execute(`UPDATE \\`${tableName}\\` SET data = ? WHERE id = ?`, [\n JSON.stringify(updatedData),\n documentId,\n ]);\n };\n\n deleteDocument: DatabaseAdapter[\"deleteDocument\"] = async (\n collectionId,\n documentId,\n _options,\n ) => {\n const tableName = this.#getTableName(collectionId);\n const [result] = await this.#connection.execute<{ affectedRows: number }>(\n `DELETE FROM \\`${tableName}\\` WHERE id = ?`,\n [documentId],\n );\n\n if (result.affectedRows === 0) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n };\n}\n"],"mappings":"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"}
|
package/dist/redis.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DatabaseAdapter } from "./~internal/adapter/database.mjs";
|
|
2
|
+
import { RedisClientType } from "redis";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/redis.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Redis database adapter for StoryBooker.
|
|
8
|
+
* Uses key prefixes to simulate collections since Redis is key-value store.
|
|
9
|
+
*
|
|
10
|
+
* Key structure: `collection:{collectionId}:doc:{documentId}`
|
|
11
|
+
* Collections list key: `collections`
|
|
12
|
+
*
|
|
13
|
+
* Compatible with all Redis services (Redis Cloud, AWS ElastiCache, Azure Cache, etc.)
|
|
14
|
+
*/
|
|
15
|
+
declare class RedisDatabaseAdapter implements DatabaseAdapter {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(client: RedisClientType, keyPrefix?: string);
|
|
18
|
+
metadata: DatabaseAdapter["metadata"];
|
|
19
|
+
init: DatabaseAdapter["init"];
|
|
20
|
+
listCollections: DatabaseAdapter["listCollections"];
|
|
21
|
+
createCollection: DatabaseAdapter["createCollection"];
|
|
22
|
+
hasCollection: DatabaseAdapter["hasCollection"];
|
|
23
|
+
deleteCollection: DatabaseAdapter["deleteCollection"];
|
|
24
|
+
listDocuments: DatabaseAdapter["listDocuments"];
|
|
25
|
+
getDocument: DatabaseAdapter["getDocument"];
|
|
26
|
+
hasDocument: DatabaseAdapter["hasDocument"];
|
|
27
|
+
createDocument: DatabaseAdapter["createDocument"];
|
|
28
|
+
updateDocument: DatabaseAdapter["updateDocument"];
|
|
29
|
+
deleteDocument: DatabaseAdapter["deleteDocument"];
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { RedisDatabaseAdapter };
|
|
33
|
+
//# sourceMappingURL=redis.d.mts.map
|
package/dist/redis.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DatabaseAdapterErrors } from "./~internal/adapter/database.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/redis.ts
|
|
4
|
+
/**
|
|
5
|
+
* Redis database adapter for StoryBooker.
|
|
6
|
+
* Uses key prefixes to simulate collections since Redis is key-value store.
|
|
7
|
+
*
|
|
8
|
+
* Key structure: `collection:{collectionId}:doc:{documentId}`
|
|
9
|
+
* Collections list key: `collections`
|
|
10
|
+
*
|
|
11
|
+
* Compatible with all Redis services (Redis Cloud, AWS ElastiCache, Azure Cache, etc.)
|
|
12
|
+
*/
|
|
13
|
+
var RedisDatabaseAdapter = class {
|
|
14
|
+
#client;
|
|
15
|
+
#keyPrefix;
|
|
16
|
+
constructor(client, keyPrefix = "sbr") {
|
|
17
|
+
this.#client = client;
|
|
18
|
+
this.#keyPrefix = keyPrefix;
|
|
19
|
+
}
|
|
20
|
+
metadata = { name: "Redis" };
|
|
21
|
+
init = async (_options) => {
|
|
22
|
+
if (!this.#client.isReady) try {
|
|
23
|
+
await this.#client.connect();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
#getDocumentKey(collectionId, documentId) {
|
|
29
|
+
return `${this.#keyPrefix}:collection:${collectionId}:doc:${documentId}`;
|
|
30
|
+
}
|
|
31
|
+
#getCollectionsSetKey() {
|
|
32
|
+
return `${this.#keyPrefix}:collections`;
|
|
33
|
+
}
|
|
34
|
+
listCollections = async (_options) => {
|
|
35
|
+
return await this.#client.sMembers(this.#getCollectionsSetKey());
|
|
36
|
+
};
|
|
37
|
+
createCollection = async (collectionId, _options) => {
|
|
38
|
+
try {
|
|
39
|
+
await this.#client.sAdd(this.#getCollectionsSetKey(), collectionId);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
hasCollection = async (collectionId, _options) => {
|
|
45
|
+
return await this.#client.sIsMember(this.#getCollectionsSetKey(), collectionId) > 0;
|
|
46
|
+
};
|
|
47
|
+
deleteCollection = async (collectionId, _options) => {
|
|
48
|
+
try {
|
|
49
|
+
const pattern = this.#getDocumentKey(collectionId, "*");
|
|
50
|
+
const keys = await this.#client.keys(pattern);
|
|
51
|
+
if (keys.length > 0) await this.#client.del(keys);
|
|
52
|
+
await this.#client.sRem(this.#getCollectionsSetKey(), collectionId);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
listDocuments = async (collectionId, listOptions, _options) => {
|
|
58
|
+
const pattern = this.#getDocumentKey(collectionId, "*");
|
|
59
|
+
const keys = await this.#client.keys(pattern);
|
|
60
|
+
if (keys.length === 0) return [];
|
|
61
|
+
const documents = (await this.#client.mGet(keys)).filter((value) => value !== null).map((value) => JSON.parse(value));
|
|
62
|
+
let filteredDocs = documents;
|
|
63
|
+
const filterFn = listOptions?.filter && typeof listOptions.filter === "function" ? listOptions.filter : void 0;
|
|
64
|
+
if (filterFn) filteredDocs = documents.filter((doc) => filterFn(doc));
|
|
65
|
+
if (listOptions?.sort) {
|
|
66
|
+
if (typeof listOptions.sort === "function") filteredDocs.sort(listOptions.sort);
|
|
67
|
+
else if (listOptions.sort === "latest") filteredDocs.sort((docA, docB) => {
|
|
68
|
+
const aTime = docA.createdAt ?? docA.timestamp ?? 0;
|
|
69
|
+
return (docB.createdAt ?? docB.timestamp ?? 0) - aTime;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (listOptions?.limit && listOptions.limit > 0) filteredDocs = filteredDocs.slice(0, listOptions.limit);
|
|
73
|
+
return filteredDocs;
|
|
74
|
+
};
|
|
75
|
+
getDocument = async (collectionId, documentId, _options) => {
|
|
76
|
+
const key = this.#getDocumentKey(collectionId, documentId);
|
|
77
|
+
const value = await this.#client.get(key);
|
|
78
|
+
if (!value) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
79
|
+
return JSON.parse(value);
|
|
80
|
+
};
|
|
81
|
+
hasDocument = async (collectionId, documentId, _options) => {
|
|
82
|
+
const key = this.#getDocumentKey(collectionId, documentId);
|
|
83
|
+
return await this.#client.exists(key) === 1;
|
|
84
|
+
};
|
|
85
|
+
createDocument = async (collectionId, documentData, _options) => {
|
|
86
|
+
try {
|
|
87
|
+
await this.#client.sAdd(this.#getCollectionsSetKey(), collectionId);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const key = this.#getDocumentKey(collectionId, documentData.id);
|
|
93
|
+
const value = JSON.stringify(documentData);
|
|
94
|
+
await this.#client.set(key, value);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(collectionId, documentData.id, error);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
updateDocument = async (collectionId, documentId, documentData) => {
|
|
100
|
+
const key = this.#getDocumentKey(collectionId, documentId);
|
|
101
|
+
const existingValue = await this.#client.get(key);
|
|
102
|
+
if (!existingValue) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
103
|
+
const updatedDoc = {
|
|
104
|
+
...JSON.parse(existingValue),
|
|
105
|
+
...documentData
|
|
106
|
+
};
|
|
107
|
+
const value = JSON.stringify(updatedDoc);
|
|
108
|
+
await this.#client.set(key, value);
|
|
109
|
+
};
|
|
110
|
+
deleteDocument = async (collectionId, documentId, _options) => {
|
|
111
|
+
const key = this.#getDocumentKey(collectionId, documentId);
|
|
112
|
+
if (await this.#client.del(key) === 0) throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { RedisDatabaseAdapter };
|
|
118
|
+
//# sourceMappingURL=redis.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.mjs","names":["#client","#keyPrefix","#getCollectionsSetKey","#getDocumentKey","documents: Document[]"],"sources":["../src/adapters/redis.ts"],"sourcesContent":["// oxlint-disable no-explicit-any\n// oxlint-disable no-unsafe-member-access\n\nimport type { RedisClientType } from \"redis\";\nimport {\n DatabaseAdapterErrors,\n type DatabaseAdapter,\n type DatabaseAdapterOptions,\n type DatabaseDocumentListOptions,\n type StoryBookerDatabaseDocument,\n} from \"./_internal/database.ts\";\n\n/**\n * Redis database adapter for StoryBooker.\n * Uses key prefixes to simulate collections since Redis is key-value store.\n *\n * Key structure: `collection:{collectionId}:doc:{documentId}`\n * Collections list key: `collections`\n *\n * Compatible with all Redis services (Redis Cloud, AWS ElastiCache, Azure Cache, etc.)\n */\nexport class RedisDatabaseAdapter implements DatabaseAdapter {\n #client: RedisClientType;\n #keyPrefix: string;\n\n constructor(client: RedisClientType, keyPrefix = \"sbr\") {\n this.#client = client;\n this.#keyPrefix = keyPrefix;\n }\n\n metadata: DatabaseAdapter[\"metadata\"] = { name: \"Redis\" };\n\n init: DatabaseAdapter[\"init\"] = async (_options) => {\n // Ensure Redis connection is ready\n if (!this.#client.isReady) {\n try {\n await this.#client.connect();\n } catch (error) {\n throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);\n }\n }\n };\n\n // Helper methods for key generation\n // #getCollectionKey(collectionId: string): string {\n // return `${this.#keyPrefix}:collection:${collectionId}`;\n // }\n\n #getDocumentKey(collectionId: string, documentId: string): string {\n return `${this.#keyPrefix}:collection:${collectionId}:doc:${documentId}`;\n }\n\n #getCollectionsSetKey(): string {\n return `${this.#keyPrefix}:collections`;\n }\n\n listCollections: DatabaseAdapter[\"listCollections\"] = async (_options) => {\n const collections = await this.#client.sMembers(this.#getCollectionsSetKey());\n return collections;\n };\n\n createCollection: DatabaseAdapter[\"createCollection\"] = async (collectionId, _options) => {\n try {\n // Add collection to the set of collections\n await this.#client.sAdd(this.#getCollectionsSetKey(), collectionId);\n } catch (error) {\n throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);\n }\n };\n\n hasCollection: DatabaseAdapter[\"hasCollection\"] = async (collectionId, _options) => {\n const exists = await this.#client.sIsMember(this.#getCollectionsSetKey(), collectionId);\n return exists > 0;\n };\n\n deleteCollection: DatabaseAdapter[\"deleteCollection\"] = async (collectionId, _options) => {\n try {\n // Get all document keys for this collection\n const pattern = this.#getDocumentKey(collectionId, \"*\");\n const keys = await this.#client.keys(pattern);\n\n // Delete all documents in the collection\n if (keys.length > 0) {\n await this.#client.del(keys);\n }\n\n // Remove collection from the set\n await this.#client.sRem(this.#getCollectionsSetKey(), collectionId);\n } catch (error) {\n throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);\n }\n };\n\n listDocuments: DatabaseAdapter[\"listDocuments\"] = async <\n Document extends StoryBookerDatabaseDocument,\n >(\n collectionId: string,\n listOptions: DatabaseDocumentListOptions<Document>,\n _options: DatabaseAdapterOptions,\n ): Promise<Document[]> => {\n const pattern = this.#getDocumentKey(collectionId, \"*\");\n const keys = await this.#client.keys(pattern);\n\n if (keys.length === 0) {\n return [];\n }\n\n // Get all documents\n const values = await this.#client.mGet(keys);\n const documents: Document[] = values\n .filter((value): value is string => value !== null)\n .map((value) => JSON.parse(value) as Document);\n\n // Apply filtering\n let filteredDocs = documents;\n const filterFn =\n listOptions?.filter && typeof listOptions.filter === \"function\"\n ? listOptions.filter\n : undefined;\n if (filterFn) {\n filteredDocs = documents.filter((doc) => filterFn(doc));\n }\n // String filters could be implemented as key pattern matching if needed\n\n // Apply sorting\n if (listOptions?.sort) {\n if (typeof listOptions.sort === \"function\") {\n filteredDocs.sort(listOptions.sort);\n } else if (listOptions.sort === \"latest\") {\n // Assume documents have a createdAt or similar timestamp field\n filteredDocs.sort((docA, docB) => {\n const aTime = (docA as any).createdAt ?? (docA as any).timestamp ?? 0;\n const bTime = (docB as any).createdAt ?? (docB as any).timestamp ?? 0;\n return bTime - aTime; // Descending order (latest first)\n });\n }\n }\n\n // Apply limit\n if (listOptions?.limit && listOptions.limit > 0) {\n filteredDocs = filteredDocs.slice(0, listOptions.limit);\n }\n\n // // Apply select (field projection)\n // if (listOptions?.select && listOptions.select.length > 0) {\n // filteredDocs = filteredDocs.map((doc) => {\n // const projected = { id: doc.id } as Document;\n // // oxlint-disable-next-line no-non-null-assertion\n // for (const field of listOptions.select!) {\n // if (field in doc) {\n // projected[field] = (doc as any)[field];\n // }\n // }\n // return projected;\n // });\n // }\n\n return filteredDocs;\n };\n\n getDocument: DatabaseAdapter[\"getDocument\"] = async <\n Document extends StoryBookerDatabaseDocument,\n >(\n collectionId: string,\n documentId: string,\n _options: DatabaseAdapterOptions,\n ): Promise<Document> => {\n const key = this.#getDocumentKey(collectionId, documentId);\n const value = await this.#client.get(key);\n\n if (!value) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n\n const document: Document = JSON.parse(value);\n return document;\n };\n\n hasDocument: DatabaseAdapter[\"hasDocument\"] = async (collectionId, documentId, _options) => {\n const key = this.#getDocumentKey(collectionId, documentId);\n const exists = await this.#client.exists(key);\n return exists === 1;\n };\n\n createDocument: DatabaseAdapter[\"createDocument\"] = async (\n collectionId,\n documentData,\n _options,\n ) => {\n // Ensure collection exists\n try {\n await this.#client.sAdd(this.#getCollectionsSetKey(), collectionId);\n } catch (error) {\n throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);\n }\n\n try {\n const key = this.#getDocumentKey(collectionId, documentData.id);\n const value = JSON.stringify(documentData);\n await this.#client.set(key, value);\n } catch (error) {\n throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(\n collectionId,\n documentData.id,\n error,\n );\n }\n };\n\n updateDocument: DatabaseAdapter[\"updateDocument\"] = async (\n collectionId,\n documentId,\n documentData,\n ) => {\n const key = this.#getDocumentKey(collectionId, documentId);\n\n // Get existing document\n const existingValue = await this.#client.get(key);\n if (!existingValue) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n\n const existingDoc = JSON.parse(existingValue);\n const updatedDoc = { ...existingDoc, ...documentData };\n\n const value = JSON.stringify(updatedDoc);\n await this.#client.set(key, value);\n };\n\n deleteDocument: DatabaseAdapter[\"deleteDocument\"] = async (\n collectionId,\n documentId,\n _options,\n ) => {\n const key = this.#getDocumentKey(collectionId, documentId);\n const deleted = await this.#client.del(key);\n\n if (deleted === 0) {\n throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { QUERY_PARAMS } from "../~internal/constants.mjs";
|
|
2
|
+
import { getStore } from "../utils/store.mjs";
|
|
3
|
+
import { urlBuilder } from "../urls.mjs";
|
|
4
|
+
import { createUIResultResponse } from "../utils/ui-utils.mjs";
|
|
5
|
+
import { openapiCommonErrorResponses, openapiResponseRedirect, openapiResponsesHtml } from "../utils/openapi-utils.mjs";
|
|
6
|
+
import { HTTPException } from "hono/http-exception";
|
|
7
|
+
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
8
|
+
|
|
9
|
+
//#region src/routers/account-router.ts
|
|
10
|
+
const accountTag = "Account";
|
|
11
|
+
/**
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
const accountRouter = new OpenAPIHono().openapi(createRoute({
|
|
15
|
+
summary: "Account page",
|
|
16
|
+
method: "get",
|
|
17
|
+
path: "/",
|
|
18
|
+
tags: [accountTag],
|
|
19
|
+
responses: {
|
|
20
|
+
200: {
|
|
21
|
+
description: "Render account page",
|
|
22
|
+
content: openapiResponsesHtml
|
|
23
|
+
},
|
|
24
|
+
...openapiCommonErrorResponses
|
|
25
|
+
}
|
|
26
|
+
}), async (context) => {
|
|
27
|
+
const { abortSignal, auth, logger, request, user, ui } = getStore();
|
|
28
|
+
if (!auth) throw new HTTPException(500, { message: "Auth is not setup" });
|
|
29
|
+
if (!user) {
|
|
30
|
+
const { pathname } = new URL(urlBuilder.account());
|
|
31
|
+
return context.redirect(urlBuilder.login(pathname), 302);
|
|
32
|
+
}
|
|
33
|
+
if (!ui?.renderAccountsPage) throw new HTTPException(405, { message: "UI is not available for this route." });
|
|
34
|
+
const children = await auth.renderAccountDetails?.(user, {
|
|
35
|
+
abortSignal,
|
|
36
|
+
logger,
|
|
37
|
+
request
|
|
38
|
+
});
|
|
39
|
+
return createUIResultResponse(context, ui.renderAccountsPage, { children });
|
|
40
|
+
}).openapi(createRoute({
|
|
41
|
+
summary: "Login to account",
|
|
42
|
+
method: "get",
|
|
43
|
+
path: "/login",
|
|
44
|
+
tags: [accountTag],
|
|
45
|
+
request: { query: z.object({ [QUERY_PARAMS.redirect]: z.string() }).partial().loose() },
|
|
46
|
+
responses: {
|
|
47
|
+
302: openapiResponseRedirect("Login successful"),
|
|
48
|
+
...openapiCommonErrorResponses
|
|
49
|
+
}
|
|
50
|
+
}), async (context) => {
|
|
51
|
+
const { abortSignal, auth, logger, request } = getStore();
|
|
52
|
+
if (!auth) throw new HTTPException(500, { message: "Auth is not setup" });
|
|
53
|
+
const response = await auth.login({
|
|
54
|
+
abortSignal,
|
|
55
|
+
logger,
|
|
56
|
+
request
|
|
57
|
+
});
|
|
58
|
+
if (response.status >= 400) return response;
|
|
59
|
+
const { redirect = "" } = context.req.valid("query");
|
|
60
|
+
const location = new URL(redirect, urlBuilder.homepage());
|
|
61
|
+
for (const [key, value] of response.headers) context.res.headers.set(key, value);
|
|
62
|
+
return context.redirect(location.toString());
|
|
63
|
+
}).openapi(createRoute({
|
|
64
|
+
summary: "Logout from account",
|
|
65
|
+
method: "get",
|
|
66
|
+
path: "/logout",
|
|
67
|
+
tags: [accountTag],
|
|
68
|
+
responses: {
|
|
69
|
+
302: openapiResponseRedirect("Logout successful"),
|
|
70
|
+
...openapiCommonErrorResponses
|
|
71
|
+
}
|
|
72
|
+
}), async (context) => {
|
|
73
|
+
const { abortSignal, auth, logger, request, user } = getStore();
|
|
74
|
+
if (!auth) throw new HTTPException(500, { message: "Auth is not setup" });
|
|
75
|
+
if (!user) throw new HTTPException(401, { message: "User is not authenticated" });
|
|
76
|
+
const response = await auth.logout(user, {
|
|
77
|
+
abortSignal,
|
|
78
|
+
logger,
|
|
79
|
+
request
|
|
80
|
+
});
|
|
81
|
+
if (response.status >= 400) return response;
|
|
82
|
+
const responseHeaders = new Headers(response.headers);
|
|
83
|
+
const responseLocation = responseHeaders.get("location");
|
|
84
|
+
responseHeaders.delete("location");
|
|
85
|
+
for (const [key, value] of responseHeaders) context.res.headers.set(key, value);
|
|
86
|
+
return context.redirect(responseLocation ?? urlBuilder.homepage());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { accountRouter };
|
|
91
|
+
//# sourceMappingURL=account-router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-router.mjs","names":[],"sources":["../../src/routers/account-router.ts"],"sourcesContent":["import { createRoute, OpenAPIHono, z } from \"@hono/zod-openapi\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { urlBuilder } from \"../urls.ts\";\nimport { QUERY_PARAMS } from \"../utils/constants.ts\";\nimport {\n openapiCommonErrorResponses,\n openapiResponseRedirect,\n openapiResponsesHtml,\n} from \"../utils/openapi-utils.ts\";\nimport { getStore } from \"../utils/store.ts\";\nimport { createUIResultResponse } from \"../utils/ui-utils.ts\";\n\nconst accountTag = \"Account\";\n\n/**\n * @private\n */\nexport const accountRouter = new OpenAPIHono()\n .openapi(\n createRoute({\n summary: \"Account page\",\n method: \"get\",\n path: \"/\",\n tags: [accountTag],\n responses: {\n 200: {\n description: \"Render account page\",\n content: openapiResponsesHtml,\n },\n ...openapiCommonErrorResponses,\n },\n }),\n async (context) => {\n const { abortSignal, auth, logger, request, user, ui } = getStore();\n\n if (!auth) {\n throw new HTTPException(500, { message: \"Auth is not setup\" });\n }\n\n if (!user) {\n const { pathname } = new URL(urlBuilder.account());\n return context.redirect(urlBuilder.login(pathname), 302);\n }\n\n if (!ui?.renderAccountsPage) {\n throw new HTTPException(405, { message: \"UI is not available for this route.\" });\n }\n\n const children = await auth.renderAccountDetails?.(user, {\n abortSignal,\n logger,\n request,\n });\n\n return createUIResultResponse(context, ui.renderAccountsPage, { children });\n },\n )\n .openapi(\n createRoute({\n summary: \"Login to account\",\n method: \"get\",\n path: \"/login\",\n tags: [accountTag],\n request: {\n query: z\n .object({ [QUERY_PARAMS.redirect]: z.string() })\n .partial()\n .loose(),\n },\n responses: {\n 302: openapiResponseRedirect(\"Login successful\"),\n ...openapiCommonErrorResponses,\n },\n }),\n async (context) => {\n const { abortSignal, auth, logger, request } = getStore();\n\n if (!auth) {\n throw new HTTPException(500, { message: \"Auth is not setup\" });\n }\n\n const response = await auth.login({ abortSignal, logger, request });\n\n if (response.status >= 400) {\n return response;\n }\n\n const { redirect = \"\" } = context.req.valid(\"query\");\n const location = new URL(redirect, urlBuilder.homepage());\n for (const [key, value] of response.headers) {\n context.res.headers.set(key, value);\n }\n\n return context.redirect(location.toString());\n },\n )\n .openapi(\n createRoute({\n summary: \"Logout from account\",\n method: \"get\",\n path: \"/logout\",\n tags: [accountTag],\n responses: {\n 302: openapiResponseRedirect(\"Logout successful\"),\n ...openapiCommonErrorResponses,\n },\n }),\n async (context) => {\n const { abortSignal, auth, logger, request, user } = getStore();\n if (!auth) {\n throw new HTTPException(500, { message: \"Auth is not setup\" });\n }\n\n if (!user) {\n throw new HTTPException(401, { message: \"User is not authenticated\" });\n }\n\n const response = await auth.logout(user, {\n abortSignal,\n logger,\n request,\n });\n if (response.status >= 400) {\n return response;\n }\n\n const responseHeaders = new Headers(response.headers);\n const responseLocation = responseHeaders.get(\"location\");\n responseHeaders.delete(\"location\");\n for (const [key, value] of responseHeaders) {\n context.res.headers.set(key, value);\n }\n\n return context.redirect(responseLocation ?? urlBuilder.homepage());\n },\n );\n"],"mappings":";;;;;;;;;AAYA,MAAM,aAAa;;;;AAKnB,MAAa,gBAAgB,IAAI,aAAa,CAC3C,QACC,YAAY;CACV,SAAS;CACT,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,WAAW;CAClB,WAAW;EACT,KAAK;GACH,aAAa;GACb,SAAS;GACV;EACD,GAAG;EACJ;CACF,CAAC,EACF,OAAO,YAAY;CACjB,MAAM,EAAE,aAAa,MAAM,QAAQ,SAAS,MAAM,OAAO,UAAU;AAEnE,KAAI,CAAC,KACH,OAAM,IAAI,cAAc,KAAK,EAAE,SAAS,qBAAqB,CAAC;AAGhE,KAAI,CAAC,MAAM;EACT,MAAM,EAAE,aAAa,IAAI,IAAI,WAAW,SAAS,CAAC;AAClD,SAAO,QAAQ,SAAS,WAAW,MAAM,SAAS,EAAE,IAAI;;AAG1D,KAAI,CAAC,IAAI,mBACP,OAAM,IAAI,cAAc,KAAK,EAAE,SAAS,uCAAuC,CAAC;CAGlF,MAAM,WAAW,MAAM,KAAK,uBAAuB,MAAM;EACvD;EACA;EACA;EACD,CAAC;AAEF,QAAO,uBAAuB,SAAS,GAAG,oBAAoB,EAAE,UAAU,CAAC;EAE9E,CACA,QACC,YAAY;CACV,SAAS;CACT,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,WAAW;CAClB,SAAS,EACP,OAAO,EACJ,OAAO,GAAG,aAAa,WAAW,EAAE,QAAQ,EAAE,CAAC,CAC/C,SAAS,CACT,OAAO,EACX;CACD,WAAW;EACT,KAAK,wBAAwB,mBAAmB;EAChD,GAAG;EACJ;CACF,CAAC,EACF,OAAO,YAAY;CACjB,MAAM,EAAE,aAAa,MAAM,QAAQ,YAAY,UAAU;AAEzD,KAAI,CAAC,KACH,OAAM,IAAI,cAAc,KAAK,EAAE,SAAS,qBAAqB,CAAC;CAGhE,MAAM,WAAW,MAAM,KAAK,MAAM;EAAE;EAAa;EAAQ;EAAS,CAAC;AAEnE,KAAI,SAAS,UAAU,IACrB,QAAO;CAGT,MAAM,EAAE,WAAW,OAAO,QAAQ,IAAI,MAAM,QAAQ;CACpD,MAAM,WAAW,IAAI,IAAI,UAAU,WAAW,UAAU,CAAC;AACzD,MAAK,MAAM,CAAC,KAAK,UAAU,SAAS,QAClC,SAAQ,IAAI,QAAQ,IAAI,KAAK,MAAM;AAGrC,QAAO,QAAQ,SAAS,SAAS,UAAU,CAAC;EAE/C,CACA,QACC,YAAY;CACV,SAAS;CACT,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,WAAW;CAClB,WAAW;EACT,KAAK,wBAAwB,oBAAoB;EACjD,GAAG;EACJ;CACF,CAAC,EACF,OAAO,YAAY;CACjB,MAAM,EAAE,aAAa,MAAM,QAAQ,SAAS,SAAS,UAAU;AAC/D,KAAI,CAAC,KACH,OAAM,IAAI,cAAc,KAAK,EAAE,SAAS,qBAAqB,CAAC;AAGhE,KAAI,CAAC,KACH,OAAM,IAAI,cAAc,KAAK,EAAE,SAAS,6BAA6B,CAAC;CAGxE,MAAM,WAAW,MAAM,KAAK,OAAO,MAAM;EACvC;EACA;EACA;EACD,CAAC;AACF,KAAI,SAAS,UAAU,IACrB,QAAO;CAGT,MAAM,kBAAkB,IAAI,QAAQ,SAAS,QAAQ;CACrD,MAAM,mBAAmB,gBAAgB,IAAI,WAAW;AACxD,iBAAgB,OAAO,WAAW;AAClC,MAAK,MAAM,CAAC,KAAK,UAAU,gBACzB,SAAQ,IAAI,QAAQ,IAAI,KAAK,MAAM;AAGrC,QAAO,QAAQ,SAAS,oBAAoB,WAAW,UAAU,CAAC;EAErE"}
|