@mushinjs/plugin-database 0.0.0-stage → 0.2.0-next.20261113103537

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Setobox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ import { Context, z } from "@mushinjs/core";
2
+ import { Database, Driver, Field, Model, Query, Selection, Tables, Types } from "minato";
3
+ export declare const name = "database";
4
+ export declare const inject: string[];
5
+ export declare const Config: z.ZodObject<{
6
+ driver: z.ZodDefault<z.ZodEnum<{
7
+ sqlite: "sqlite";
8
+ memory: "memory";
9
+ }>>;
10
+ path: z.ZodDefault<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ export type Config = z.infer<typeof Config>;
13
+ export interface ColumnInfo {
14
+ name: string;
15
+ type: string;
16
+ primary: boolean;
17
+ nullable: boolean;
18
+ }
19
+ export interface TableInfo {
20
+ name: string;
21
+ /** The part of the name before the first underscore, which by convention names the plugin. */
22
+ group: string;
23
+ primary: string[];
24
+ columns: ColumnInfo[];
25
+ count: number;
26
+ }
27
+ export declare function toCsv(columns: string[], rows: Record<string, unknown>[]): string;
28
+ /**
29
+ * The database service (`ctx.database`, from minato) and the actions behind the Database page.
30
+ * Plugins declare tables with `ctx.model.extend()` in a context that injects both `model` and
31
+ * `database`; by convention a table's name starts with the plugin's name and an underscore.
32
+ */
33
+ export declare function apply(ctx: Context, config: Config): void;
34
+ export type { Database, Driver, Field, Model, Query, Selection, Tables, Types };
package/dist/index.js ADDED
@@ -0,0 +1,269 @@
1
+ import { mkdir, stat } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { MushinError, field, z } from "@mushinjs/core";
5
+ import MemoryDriver from "@minatojs/driver-memory";
6
+ import SQLiteDriver from "@minatojs/driver-sqlite";
7
+ import { $, Database } from "minato";
8
+ // src/index.ts
9
+ const name = "database";
10
+ const inject = ["actions"];
11
+ const Config = z.object({
12
+ driver: z.enum(["sqlite", "memory"]).default("sqlite").meta(field("Driver", "sqlite keeps everything in one file; memory loses the data when Mushin.js stops (for trying things out)", { "zh-CN": {
13
+ title: "驱动",
14
+ description: "sqlite 把数据存在一个文件里;memory 在 Mushin.js 停止时丢失数据(用于试用)"
15
+ } })),
16
+ path: z.string().trim().min(1).default("data/mushin.db").meta(field("Database file", "For SQLite; relative to the instance directory", { "zh-CN": {
17
+ title: "数据库文件",
18
+ description: "SQLite 使用;相对于实例目录"
19
+ } }))
20
+ });
21
+ var SQLite = class extends SQLiteDriver {
22
+ static inject = ["model"];
23
+ };
24
+ var Memory = class extends MemoryDriver {
25
+ static inject = ["model"];
26
+ };
27
+ const PAGE_LIMIT = 500;
28
+ const EXPORT_LIMIT = 1e5;
29
+ /** Rows as JSON can carry them: dates as ISO text, binary as a note, big integers as text. */
30
+ function plain(value) {
31
+ if (value instanceof Date) return value.toISOString();
32
+ if (typeof value === "bigint") return value.toString();
33
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return `<binary ${value.byteLength} bytes>`;
34
+ if (Array.isArray(value)) return value.map(plain);
35
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
36
+ return value;
37
+ }
38
+ function csvCell(value) {
39
+ if (value === void 0 || value === null) return "";
40
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
41
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
42
+ }
43
+ function toCsv(columns, rows) {
44
+ return [columns.map(csvCell).join(","), ...rows.map((row) => columns.map((column) => csvCell(row[column])).join(","))].join("\r\n") + "\r\n";
45
+ }
46
+ const numeric = /* @__PURE__ */ new Set([
47
+ "integer",
48
+ "unsigned",
49
+ "float",
50
+ "double",
51
+ "decimal",
52
+ "bigint"
53
+ ]);
54
+ const textual = /* @__PURE__ */ new Set([
55
+ "char",
56
+ "string",
57
+ "text"
58
+ ]);
59
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ const Filter = z.record(z.string(), z.string()).optional().describe("Column → text. Text columns match when they contain the text; numbers and booleans must be equal.");
61
+ /**
62
+ * The database service (`ctx.database`, from minato) and the actions behind the Database page.
63
+ * Plugins declare tables with `ctx.model.extend()` in a context that injects both `model` and
64
+ * `database`; by convention a table's name starts with the plugin's name and an underscore.
65
+ */
66
+ function apply(ctx, config) {
67
+ const baseDir = ctx.baseDir ?? process.cwd();
68
+ const file = config.driver === "sqlite" ? isAbsolute(config.path) ? config.path : resolve(baseDir, config.path) : void 0;
69
+ ctx.plugin(Database);
70
+ if (file) ctx.inject(["model"], async (ctx) => {
71
+ await mkdir(dirname(file), { recursive: true });
72
+ ctx.plugin(SQLite, { path: pathToFileURL(file).href });
73
+ });
74
+ else ctx.plugin(Memory, {});
75
+ ctx.inject(["model", "database"], (ctx) => {
76
+ const tableOf = (name) => {
77
+ const model = ctx.model.tables[name];
78
+ if (!model) throw new MushinError("not_found", `No table named "${name}"`);
79
+ return model;
80
+ };
81
+ const primaryOf = (model) => Array.isArray(model.primary) ? model.primary : [model.primary];
82
+ const columnsOf = (model) => {
83
+ const primary = primaryOf(model);
84
+ return Object.entries(model.fields).filter(([, item]) => item && !item.deprecated && !item.relation && !item.expr).map(([name, item]) => ({
85
+ name,
86
+ type: String(item.deftype ?? item.type?.type ?? ""),
87
+ primary: primary.includes(name),
88
+ nullable: !!item.nullable
89
+ }));
90
+ };
91
+ const queryOf = (model, filter) => {
92
+ const query = {};
93
+ const columns = new Map(columnsOf(model).map((column) => [column.name, column]));
94
+ for (const [name, text] of Object.entries(filter ?? {})) {
95
+ if (text === "") continue;
96
+ const column = columns.get(name);
97
+ if (!column) throw new MushinError("invalid_input", `Table "${model.name}" has no column "${name}"`);
98
+ if (numeric.has(column.type)) {
99
+ const value = Number(text);
100
+ if (!Number.isFinite(value)) throw new MushinError("invalid_input", `"${text}" is not a number`);
101
+ query[name] = value;
102
+ } else if (column.type === "boolean") query[name] = text === "true" || text === "1";
103
+ else if (textual.has(column.type)) query[name] = { $regex: new RegExp(escapeRegExp(text), "i") };
104
+ else throw new MushinError("invalid_input", `Column "${name}" (${column.type}) cannot be filtered`);
105
+ }
106
+ return query;
107
+ };
108
+ const keyQuery = (model, key) => {
109
+ const query = {};
110
+ for (const name of primaryOf(model)) {
111
+ if (!(name in key)) throw new MushinError("invalid_input", `The key needs "${name}"`);
112
+ query[name] = key[name];
113
+ }
114
+ return query;
115
+ };
116
+ const status = async () => {
117
+ const size = file ? (await stat(file).catch(() => void 0))?.size ?? 0 : void 0;
118
+ return {
119
+ driver: config.driver,
120
+ ...file ? {
121
+ path: relative(baseDir, file).split(sep).join("/"),
122
+ size
123
+ } : {},
124
+ tables: Object.keys(ctx.model.tables).length
125
+ };
126
+ };
127
+ const countRows = async (table, model, query) => {
128
+ const key = primaryOf(model)[0] ?? columnsOf(model)[0]?.name;
129
+ return Number(await ctx.database.select(table, query).execute((row) => $.count(row[key]))) || 0;
130
+ };
131
+ const tables = async () => {
132
+ const list = [];
133
+ for (const [name, model] of Object.entries(ctx.model.tables)) {
134
+ const count = await countRows(name, model, {});
135
+ list.push({
136
+ name,
137
+ group: name.split("_")[0],
138
+ primary: primaryOf(model),
139
+ columns: columnsOf(model),
140
+ count: Number(count) || 0
141
+ });
142
+ }
143
+ return list.sort((a, b) => a.name.localeCompare(b.name));
144
+ };
145
+ ctx.actions.define({
146
+ name: "database.status",
147
+ description: "Which database driver runs, the SQLite file and its size, and how many tables exist.",
148
+ risk: "read",
149
+ input: z.object({}),
150
+ handler: status
151
+ });
152
+ ctx.actions.define({
153
+ name: "database.tables",
154
+ description: "Every table in the database with its columns, primary key and row count. Tables are grouped by the plugin name before the first underscore, e.g. flows_var.",
155
+ risk: "read",
156
+ input: z.object({}),
157
+ handler: tables
158
+ });
159
+ ctx.actions.define({
160
+ name: "database.rows",
161
+ description: "A page of rows from one table, read-only: optional sort and filters. Returns the rows and the total that match.",
162
+ risk: "read",
163
+ input: z.object({
164
+ table: z.string(),
165
+ offset: z.number().int().min(0).default(0),
166
+ limit: z.number().int().min(1).max(PAGE_LIMIT).default(50),
167
+ sort: z.object({
168
+ column: z.string(),
169
+ order: z.enum(["asc", "desc"]).default("asc")
170
+ }).optional(),
171
+ filter: Filter
172
+ }),
173
+ handler: async ({ table, offset, limit, sort, filter }) => {
174
+ const model = tableOf(table);
175
+ const query = queryOf(model, filter);
176
+ let selection = ctx.database.select(table, query);
177
+ if (sort) {
178
+ if (!columnsOf(model).some((column) => column.name === sort.column)) throw new MushinError("invalid_input", `Table "${table}" has no column "${sort.column}"`);
179
+ selection = selection.orderBy(sort.column, sort.order);
180
+ }
181
+ const [rows, total] = await Promise.all([selection.offset(offset).limit(limit).execute(), countRows(table, model, query)]);
182
+ return {
183
+ rows: rows.map((row) => plain(row)),
184
+ total
185
+ };
186
+ }
187
+ });
188
+ ctx.actions.define({
189
+ name: "database.update",
190
+ description: "Changes columns of one row, found by its primary key. Writes to plugin data directly, so the change takes effect without any checks the plugin would make.",
191
+ risk: "dangerous",
192
+ input: z.object({
193
+ table: z.string(),
194
+ key: z.record(z.string(), z.unknown()),
195
+ data: z.record(z.string(), z.unknown())
196
+ }),
197
+ handler: async ({ table, key, data }) => {
198
+ const model = tableOf(table);
199
+ const columns = new Set(columnsOf(model).map((column) => column.name));
200
+ for (const name of Object.keys(data)) {
201
+ if (!columns.has(name)) throw new MushinError("invalid_input", `Table "${table}" has no column "${name}"`);
202
+ if (primaryOf(model).includes(name)) throw new MushinError("invalid_input", `"${name}" is part of the primary key and cannot be changed`);
203
+ }
204
+ const result = await ctx.database.set(table, keyQuery(model, key), data);
205
+ if (!result.matched) throw new MushinError("not_found", "No row has that key");
206
+ return { updated: result.modified ?? result.matched };
207
+ }
208
+ });
209
+ ctx.actions.define({
210
+ name: "database.remove",
211
+ description: "Deletes rows from a table by their primary keys. Cannot be undone; make a backup first if unsure.",
212
+ risk: "dangerous",
213
+ input: z.object({
214
+ table: z.string(),
215
+ keys: z.array(z.record(z.string(), z.unknown())).min(1).max(PAGE_LIMIT)
216
+ }),
217
+ handler: async ({ table, keys }) => {
218
+ const model = tableOf(table);
219
+ let removed = 0;
220
+ for (const key of keys) removed += (await ctx.database.remove(table, keyQuery(model, key))).removed ?? 0;
221
+ return { removed };
222
+ }
223
+ });
224
+ ctx.actions.define({
225
+ name: "database.export",
226
+ description: "The rows of a table (matching optional filters) as CSV or JSON text, for downloading.",
227
+ risk: "read",
228
+ input: z.object({
229
+ table: z.string(),
230
+ format: z.enum(["csv", "json"]).default("csv"),
231
+ filter: Filter
232
+ }),
233
+ handler: async ({ table, format, filter }) => {
234
+ const model = tableOf(table);
235
+ const rows = (await ctx.database.select(table, queryOf(model, filter)).limit(EXPORT_LIMIT).execute()).map((row) => plain(row));
236
+ const columns = columnsOf(model).map((column) => column.name);
237
+ const content = format === "csv" ? "" + toCsv(columns, rows) : JSON.stringify(rows, null, 2);
238
+ return {
239
+ filename: `${table}.${format}`,
240
+ content,
241
+ rows: rows.length,
242
+ truncated: rows.length >= EXPORT_LIMIT
243
+ };
244
+ }
245
+ });
246
+ ctx.actions.define({
247
+ name: "database.backup",
248
+ description: "Copies the SQLite database to data/backups/ under a dated name while Mushin.js keeps running. Returns the backup's path.",
249
+ risk: "write",
250
+ input: z.object({}),
251
+ handler: async () => {
252
+ const driver = ctx.database._driver;
253
+ if (!file || !driver?.db) throw new MushinError("unavailable", "Backups need the SQLite driver");
254
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
255
+ const target = join(dirname(file), "backups", `mushin-${stamp}.db`);
256
+ await mkdir(dirname(target), { recursive: true });
257
+ driver.db.exec(`VACUUM INTO '${target.replace(/'/g, "''")}'`);
258
+ const size = (await stat(target)).size;
259
+ return {
260
+ path: relative(baseDir, target).split(sep).join("/"),
261
+ size
262
+ };
263
+ }
264
+ });
265
+ });
266
+ }
267
+ export { Config, apply, inject, name, toCsv };
268
+
269
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { mkdir, stat } from 'node:fs/promises'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { type Context, field, MushinError, z } from '@mushinjs/core'\nimport MemoryDriver from '@minatojs/driver-memory'\nimport SQLiteDriver from '@minatojs/driver-sqlite'\nimport { $, Database, type Field, type Model } from 'minato'\n\nexport type { Database, Driver, Field, Model, Query, Selection, Tables, Types } from 'minato'\n\nexport const name = 'database'\nexport const inject = ['actions']\n\nexport const Config = z.object({\n driver: z.enum(['sqlite', 'memory']).default('sqlite').meta(field('Driver', 'sqlite keeps everything in one file; memory loses the data when Mushin.js stops (for trying things out)', {\n 'zh-CN': { title: '驱动', description: 'sqlite 把数据存在一个文件里;memory 在 Mushin.js 停止时丢失数据(用于试用)' },\n })),\n path: z.string().trim().min(1).default('data/mushin.db').meta(field('Database file', 'For SQLite; relative to the instance directory', {\n 'zh-CN': { title: '数据库文件', description: 'SQLite 使用;相对于实例目录' },\n })),\n})\n\nexport type Config = z.infer<typeof Config>\n\n// The drivers inject a `logger` service that Cordis 4 keeps as a built-in property instead, so\n// they would wait for it forever. They only call `ctx.logger?.info()` and the like, which the\n// built-in logger answers, so only the model is injected here.\nclass SQLite extends (SQLiteDriver as any) {\n static inject = ['model']\n}\n\nclass Memory extends (MemoryDriver as any) {\n static inject = ['model']\n}\n\nconst PAGE_LIMIT = 500\nconst EXPORT_LIMIT = 100_000\n\nexport interface ColumnInfo {\n name: string\n type: string\n primary: boolean\n nullable: boolean\n}\n\nexport interface TableInfo {\n name: string\n /** The part of the name before the first underscore, which by convention names the plugin. */\n group: string\n primary: string[]\n columns: ColumnInfo[]\n count: number\n}\n\n/** Rows as JSON can carry them: dates as ISO text, binary as a note, big integers as text. */\nfunction plain(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'bigint') return value.toString()\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return `<binary ${(value as ArrayBuffer).byteLength} bytes>`\n if (Array.isArray(value)) return value.map(plain)\n if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]))\n return value\n}\n\nfunction csvCell(value: unknown) {\n if (value === undefined || value === null) return ''\n const text = typeof value === 'object' ? JSON.stringify(value) : String(value)\n return /[\",\\r\\n]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text\n}\n\nexport function toCsv(columns: string[], rows: Record<string, unknown>[]) {\n return [columns.map(csvCell).join(','), ...rows.map((row) => columns.map((column) => csvCell(row[column])).join(','))].join('\\r\\n') + '\\r\\n'\n}\n\nconst numeric = new Set(['integer', 'unsigned', 'float', 'double', 'decimal', 'bigint'])\nconst textual = new Set(['char', 'string', 'text'])\n\nconst escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\nconst Filter = z.record(z.string(), z.string()).optional().describe('Column → text. Text columns match when they contain the text; numbers and booleans must be equal.')\n\n/**\n * The database service (`ctx.database`, from minato) and the actions behind the Database page.\n * Plugins declare tables with `ctx.model.extend()` in a context that injects both `model` and\n * `database`; by convention a table's name starts with the plugin's name and an underscore.\n */\nexport function apply(ctx: Context, config: Config) {\n const baseDir = ctx.baseDir ?? process.cwd()\n const file = config.driver === 'sqlite' ? (isAbsolute(config.path) ? config.path : resolve(baseDir, config.path)) : undefined\n\n ctx.plugin(Database)\n if (file) {\n ctx.inject(['model'], async (ctx) => {\n await mkdir(dirname(file), { recursive: true })\n // The driver resolves the path against `ctx.baseUrl`; a file URL resolves to itself.\n ctx.plugin(SQLite as any, { path: pathToFileURL(file).href })\n })\n } else {\n ctx.plugin(Memory as any, {})\n }\n\n ctx.inject(['model', 'database'], (ctx) => {\n const tableOf = (name: string): Model => {\n const model = ctx.model.tables[name]\n if (!model) throw new MushinError('not_found', `No table named \"${name}\"`)\n return model\n }\n const primaryOf = (model: Model) => (Array.isArray(model.primary) ? model.primary : [model.primary]) as string[]\n const columnsOf = (model: Model): ColumnInfo[] => {\n const primary = primaryOf(model)\n return Object.entries(model.fields as Record<string, Field | undefined>)\n .filter(([, item]) => item && !item.deprecated && !item.relation && !item.expr)\n .map(([name, item]) => ({ name, type: String(item!.deftype ?? item!.type?.type ?? ''), primary: primary.includes(name), nullable: !!item!.nullable }))\n }\n const queryOf = (model: Model, filter: Record<string, string> | undefined) => {\n const query: Record<string, unknown> = {}\n const columns = new Map(columnsOf(model).map((column) => [column.name, column]))\n for (const [name, text] of Object.entries(filter ?? {})) {\n if (text === '') continue\n const column = columns.get(name)\n if (!column) throw new MushinError('invalid_input', `Table \"${model.name}\" has no column \"${name}\"`)\n if (numeric.has(column.type)) {\n const value = Number(text)\n if (!Number.isFinite(value)) throw new MushinError('invalid_input', `\"${text}\" is not a number`)\n query[name] = value\n } else if (column.type === 'boolean') {\n query[name] = text === 'true' || text === '1'\n } else if (textual.has(column.type)) {\n query[name] = { $regex: new RegExp(escapeRegExp(text), 'i') }\n } else {\n throw new MushinError('invalid_input', `Column \"${name}\" (${column.type}) cannot be filtered`)\n }\n }\n return query\n }\n const keyQuery = (model: Model, key: Record<string, unknown>) => {\n const query: Record<string, unknown> = {}\n for (const name of primaryOf(model)) {\n if (!(name in key)) throw new MushinError('invalid_input', `The key needs \"${name}\"`)\n query[name] = key[name]\n }\n return query\n }\n\n const status = async () => {\n const size = file ? (await stat(file).catch(() => undefined))?.size ?? 0 : undefined\n return {\n driver: config.driver,\n ...(file ? { path: relative(baseDir, file).split(sep).join('/'), size } : {}),\n tables: Object.keys(ctx.model.tables).length,\n }\n }\n\n const countRows = async (table: string, model: Model, query: Record<string, unknown>) => {\n const key = primaryOf(model)[0] ?? columnsOf(model)[0]?.name\n return Number(await ctx.database.select(table as any, query as any).execute((row: any) => $.count(row[key]))) || 0\n }\n\n const tables = async (): Promise<TableInfo[]> => {\n const list: TableInfo[] = []\n for (const [name, model] of Object.entries(ctx.model.tables)) {\n const count = await countRows(name, model, {})\n list.push({ name, group: name.split('_')[0], primary: primaryOf(model), columns: columnsOf(model), count: Number(count) || 0 })\n }\n return list.sort((a, b) => a.name.localeCompare(b.name))\n }\n\n ctx.actions.define({\n name: 'database.status',\n description: 'Which database driver runs, the SQLite file and its size, and how many tables exist.',\n risk: 'read',\n input: z.object({}),\n handler: status,\n })\n\n ctx.actions.define({\n name: 'database.tables',\n description: 'Every table in the database with its columns, primary key and row count. Tables are grouped by the plugin name before the first underscore, e.g. flows_var.',\n risk: 'read',\n input: z.object({}),\n handler: tables,\n })\n\n ctx.actions.define({\n name: 'database.rows',\n description: 'A page of rows from one table, read-only: optional sort and filters. Returns the rows and the total that match.',\n risk: 'read',\n input: z.object({\n table: z.string(),\n offset: z.number().int().min(0).default(0),\n limit: z.number().int().min(1).max(PAGE_LIMIT).default(50),\n sort: z.object({ column: z.string(), order: z.enum(['asc', 'desc']).default('asc') }).optional(),\n filter: Filter,\n }),\n handler: async ({ table, offset, limit, sort, filter }) => {\n const model = tableOf(table)\n const query = queryOf(model, filter)\n let selection = ctx.database.select(table as any, query as any)\n if (sort) {\n if (!columnsOf(model).some((column) => column.name === sort.column)) throw new MushinError('invalid_input', `Table \"${table}\" has no column \"${sort.column}\"`)\n selection = selection.orderBy(sort.column as any, sort.order)\n }\n const [rows, total] = await Promise.all([\n selection.offset(offset).limit(limit).execute(),\n countRows(table, model, query),\n ])\n return { rows: rows.map((row) => plain(row)), total }\n },\n })\n\n ctx.actions.define({\n name: 'database.update',\n description: 'Changes columns of one row, found by its primary key. Writes to plugin data directly, so the change takes effect without any checks the plugin would make.',\n risk: 'dangerous',\n input: z.object({ table: z.string(), key: z.record(z.string(), z.unknown()), data: z.record(z.string(), z.unknown()) }),\n handler: async ({ table, key, data }) => {\n const model = tableOf(table)\n const columns = new Set(columnsOf(model).map((column) => column.name))\n for (const name of Object.keys(data)) {\n if (!columns.has(name)) throw new MushinError('invalid_input', `Table \"${table}\" has no column \"${name}\"`)\n if (primaryOf(model).includes(name)) throw new MushinError('invalid_input', `\"${name}\" is part of the primary key and cannot be changed`)\n }\n const result = await ctx.database.set(table as any, keyQuery(model, key) as any, data as any)\n if (!result.matched) throw new MushinError('not_found', 'No row has that key')\n return { updated: result.modified ?? result.matched }\n },\n })\n\n ctx.actions.define({\n name: 'database.remove',\n description: 'Deletes rows from a table by their primary keys. Cannot be undone; make a backup first if unsure.',\n risk: 'dangerous',\n input: z.object({ table: z.string(), keys: z.array(z.record(z.string(), z.unknown())).min(1).max(PAGE_LIMIT) }),\n handler: async ({ table, keys }) => {\n const model = tableOf(table)\n let removed = 0\n for (const key of keys) removed += (await ctx.database.remove(table as any, keyQuery(model, key) as any)).removed ?? 0\n return { removed }\n },\n })\n\n ctx.actions.define({\n name: 'database.export',\n description: 'The rows of a table (matching optional filters) as CSV or JSON text, for downloading.',\n risk: 'read',\n input: z.object({ table: z.string(), format: z.enum(['csv', 'json']).default('csv'), filter: Filter }),\n handler: async ({ table, format, filter }) => {\n const model = tableOf(table)\n const rows = (await ctx.database.select(table as any, queryOf(model, filter) as any).limit(EXPORT_LIMIT).execute()).map((row) => plain(row) as Record<string, unknown>)\n const columns = columnsOf(model).map((column) => column.name)\n const content = format === 'csv' ? '' + toCsv(columns, rows) : JSON.stringify(rows, null, 2)\n return { filename: `${table}.${format}`, content, rows: rows.length, truncated: rows.length >= EXPORT_LIMIT }\n },\n })\n\n ctx.actions.define({\n name: 'database.backup',\n description: 'Copies the SQLite database to data/backups/ under a dated name while Mushin.js keeps running. Returns the backup\\'s path.',\n risk: 'write',\n input: z.object({}),\n handler: async () => {\n const driver = (ctx.database as any)._driver\n if (!file || !driver?.db) throw new MushinError('unavailable', 'Backups need the SQLite driver')\n const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15)\n const target = join(dirname(file), 'backups', `mushin-${stamp}.db`)\n await mkdir(dirname(target), { recursive: true })\n driver.db.exec(`VACUUM INTO '${target.replace(/'/g, \"''\")}'`)\n const size = (await stat(target)).size\n return { path: relative(baseDir, target).split(sep).join('/'), size }\n },\n })\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@mushinjs/plugin-database",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-next.20261113103537",
4
+ "description": "A database for plugins, SQLite by default",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mushinjs/mushin.git",
10
+ "directory": "packages/plugin-database"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "keywords": [
26
+ "mushin-plugin"
27
+ ],
28
+ "mushin": {
29
+ "kind": "app",
30
+ "displayName": {
31
+ "en": "Database",
32
+ "zh-CN": "数据库"
33
+ },
34
+ "description": {
35
+ "en": "Keeps flow variables, messages and other plugins' tables in a database (SQLite by default), with a page to browse them",
36
+ "zh-CN": "把流程变量、消息和其他插件的数据表存进数据库(默认 SQLite),并提供浏览页面"
37
+ },
38
+ "provides": [
39
+ "database",
40
+ "model"
41
+ ],
42
+ "requires": {
43
+ "mushin": ">=0.1.0"
44
+ }
45
+ },
46
+ "dependencies": {
47
+ "@minatojs/driver-memory": "4.0.0",
48
+ "@minatojs/driver-sqlite": "5.0.3",
49
+ "@mushinjs/core": "0.2.0-next.20261113103537",
50
+ "cordis": "4.0.0-rc.11",
51
+ "minato": "4.0.1"
52
+ }
6
53
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.