@tiddlywiki/mws 0.0.12 → 0.0.14
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/.env-cmdrc +5 -0
- package/README.md +3 -1
- package/dist/server.d.ts +923 -0
- package/dist/server.js +1763 -1415
- package/dist/server.js.map +1 -1
- package/editions/mws-docs/tiddlers/$__StoryList.tid +3 -3
- package/editions/mws-docs/tiddlers/MWS and SQLite.md +11 -0
- package/editions/mws-docs/tiddlers/Request Server.md +20 -0
- package/editions/mws-docs/tiddlers/Request Server.md.meta +5 -0
- package/package.json +17 -10
- package/prisma/schema.prisma +1 -0
- package/react-user-mgmt/package-lock.json +76 -321
- package/react-user-mgmt/package.json +2 -5
- package/react-user-mgmt/public/chunk-YALWN5OT.js +2 -0
- package/react-user-mgmt/public/chunk-YALWN5OT.js.map +7 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js +3 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js.map +7 -0
- package/react-user-mgmt/public/index.html +1 -26
- package/react-user-mgmt/public/main.js +68 -109
- package/react-user-mgmt/public/main.js.map +4 -4
- package/react-user-mgmt/public/stats.css +597 -0
- package/react-user-mgmt/public/stats.html +59 -0
- package/react-user-mgmt/public/stats.js +2013 -0
- package/react-user-mgmt/public/stats.json +1 -0
- package/react-user-mgmt/public/stats.svg +4 -0
- package/react-user-mgmt/tsconfig.json +3 -0
- package/dist/server.cjs +0 -16677
- package/dist/server.cjs.map +0 -1
- package/src/StateObject.ts +0 -316
- package/src/commander.ts +0 -417
- package/src/commands/divider.ts +0 -20
- package/src/commands/index.ts +0 -24
- package/src/commands/mws-init-store.ts +0 -77
- package/src/commands/mws-listen.ts +0 -136
- package/src/commands/mws-load-archive.ts +0 -281
- package/src/commands/mws-load-plugin-bags.ts +0 -151
- package/src/commands/mws-load-tiddlers.ts +0 -37
- package/src/commands/mws-load-wiki-folder.ts +0 -124
- package/src/commands/mws-render-tiddlywiki5.ts +0 -55
- package/src/commands/mws-save-archive.ts +0 -111
- package/src/commands/mws-save-tiddler-text.ts +0 -33
- package/src/commands/tests-complete.ts +0 -21
- package/src/global.ts +0 -247
- package/src/routes/TiddlerStore.ts +0 -722
- package/src/routes/bag-file-server.ts +0 -340
- package/src/routes/cache.ts +0 -25
- package/src/routes/index.ts +0 -138
- package/src/routes/managers/index.ts +0 -17
- package/src/routes/managers/manager-recipes.ts +0 -438
- package/src/routes/managers/manager-users.ts +0 -201
- package/src/routes/managers/router-tiddlers.ts +0 -313
- package/src/routes/router.ts +0 -429
- package/src/routes/services/PasswordService.ts +0 -178
- package/src/routes/services/attachments.ts +0 -259
- package/src/routes/services/sessions.ts +0 -166
- package/src/routes/tw-test.ts +0 -65
- package/src/server.ts +0 -151
- package/src/setupDevServer.ts +0 -71
- package/src/streamer.ts +0 -430
- package/src/tiddlywiki.ts +0 -94
- package/src/utils/data-checks.ts +0 -172
- package/src/utils/index.ts +0 -4
- package/src/utils/rootRoute.ts +0 -237
- package/src/utils/utils.ts +0 -403
- package/src/utils/zodAssert.ts +0 -269
- /package/prisma/{migrations/notes.md → notes.md} +0 -0
|
@@ -1,722 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { SiteConfig } from "./router";
|
|
4
|
-
import { AttachmentService, TiddlerFields } from "./services/attachments";
|
|
5
|
-
import { ok } from "assert";
|
|
6
|
-
import { Commander } from "../commander";
|
|
7
|
-
import { FileInfoTiddlers } from "tiddlywiki";
|
|
8
|
-
import { UserError } from "../utils";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
### NOTES from attachments file
|
|
13
|
-
|
|
14
|
-
Class to handle the attachments in the filing system
|
|
15
|
-
|
|
16
|
-
The store folder looks like this:
|
|
17
|
-
|
|
18
|
-
store/
|
|
19
|
-
inbox/ - files that are in the process of being uploaded via a multipart form upload
|
|
20
|
-
202402282125432742/
|
|
21
|
-
0
|
|
22
|
-
1
|
|
23
|
-
...
|
|
24
|
-
...
|
|
25
|
-
files/ - files that are the text content of large tiddlers
|
|
26
|
-
b7def178-79c4-4d88-b7a4-39763014a58b/
|
|
27
|
-
data.jpg - the extension is provided for convenience when directly inspecting the file system
|
|
28
|
-
meta.json - contains:
|
|
29
|
-
{
|
|
30
|
-
"filename": "data.jpg",
|
|
31
|
-
"type": "video/mp4",
|
|
32
|
-
"uploaded": "2024021821224823"
|
|
33
|
-
}
|
|
34
|
-
database.sql - The database file (managed by Prisma)
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
export class TiddlerStore {
|
|
38
|
-
attachService: AttachmentService;
|
|
39
|
-
storePath: string;
|
|
40
|
-
config;
|
|
41
|
-
fieldModules;
|
|
42
|
-
constructor(
|
|
43
|
-
private commander: Commander,
|
|
44
|
-
public prisma: PrismaTxnClient
|
|
45
|
-
) {
|
|
46
|
-
this.fieldModules = this.commander.$tw.Tiddler.fieldModules;
|
|
47
|
-
this.attachService = new commander.AttachmentService(commander.siteConfig, prisma);
|
|
48
|
-
this.storePath = commander.siteConfig.storePath;
|
|
49
|
-
this.config = commander.siteConfig;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/*
|
|
53
|
-
Returns null if a bag/recipe name is valid, or a string error message if not
|
|
54
|
-
*/
|
|
55
|
-
validateItemName(name: string, allowPrivilegedCharacters: boolean) {
|
|
56
|
-
if (typeof name !== "string") {
|
|
57
|
-
return "Not a valid string";
|
|
58
|
-
}
|
|
59
|
-
if (name.length > 256) {
|
|
60
|
-
return "Too long";
|
|
61
|
-
}
|
|
62
|
-
// Removed ~ from this list temporarily
|
|
63
|
-
if (allowPrivilegedCharacters) {
|
|
64
|
-
if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) {
|
|
65
|
-
return "Invalid character(s)";
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) {
|
|
69
|
-
return "Invalid character(s)";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
/*
|
|
75
|
-
Returns null if the argument is an array of valid bag/recipe names, or a string error message if not
|
|
76
|
-
*/
|
|
77
|
-
validateItemNames(names: string[], allowPrivilegedCharacters: boolean) {
|
|
78
|
-
if (!Array.isArray(names)) {
|
|
79
|
-
return "Not a valid array";
|
|
80
|
-
}
|
|
81
|
-
var errors = [];
|
|
82
|
-
for (const name of names) {
|
|
83
|
-
const result = this.validateItemName(name, allowPrivilegedCharacters);
|
|
84
|
-
if (result && errors.indexOf(result) === -1) {
|
|
85
|
-
errors.push(result);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (errors.length === 0) {
|
|
89
|
-
return null;
|
|
90
|
-
} else {
|
|
91
|
-
return errors.join("\n");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
async upsertRecipe(
|
|
95
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">,
|
|
96
|
-
description: PrismaField<"Recipes", "description">,
|
|
97
|
-
bags: { bag_name: PrismaField<"Bags", "bag_name">, with_acl: PrismaField<"Recipe_bags", "with_acl"> }[],
|
|
98
|
-
{ allowPrivilegedCharacters = false }: { allowPrivilegedCharacters?: boolean; } = {}
|
|
99
|
-
) {
|
|
100
|
-
const [recipe_, recipe_bags_] = this.upsertRecipe_PrismaArray(recipe_name, description, bags, { allowPrivilegedCharacters });
|
|
101
|
-
const recipe = await recipe_;
|
|
102
|
-
await recipe_bags_;
|
|
103
|
-
return recipe;
|
|
104
|
-
}
|
|
105
|
-
upsertRecipe_PrismaArray(
|
|
106
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">,
|
|
107
|
-
description: PrismaField<"Recipes", "description">,
|
|
108
|
-
bags: { bag_name: PrismaField<"Bags", "bag_name">, with_acl: PrismaField<"Recipe_bags", "with_acl"> }[],
|
|
109
|
-
{ allowPrivilegedCharacters = false }: { allowPrivilegedCharacters?: boolean; } = {}
|
|
110
|
-
) {
|
|
111
|
-
|
|
112
|
-
const validationRecipeName = this.validateItemName(recipe_name, allowPrivilegedCharacters);
|
|
113
|
-
if (validationRecipeName) throw validationRecipeName;
|
|
114
|
-
if (bags.length === 0) throw new UserError("Recipes must contain at least one bag");
|
|
115
|
-
|
|
116
|
-
return [
|
|
117
|
-
this.prisma.recipes.upsert({
|
|
118
|
-
where: { recipe_name },
|
|
119
|
-
update: { description, recipe_bags: { deleteMany: {} } },
|
|
120
|
-
create: { recipe_name, description },
|
|
121
|
-
select: { recipe_id: true }
|
|
122
|
-
}),
|
|
123
|
-
this.prisma.recipes.update({
|
|
124
|
-
where: { recipe_name },
|
|
125
|
-
data: {
|
|
126
|
-
recipe_bags: {
|
|
127
|
-
create: bags.map(({ bag_name, with_acl }, position) => ({
|
|
128
|
-
bag: { connect: { bag_name } },
|
|
129
|
-
position,
|
|
130
|
-
with_acl
|
|
131
|
-
}))
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
async upsertBag(
|
|
139
|
-
bag_name: PrismaField<"Bags", "bag_name">,
|
|
140
|
-
description: PrismaField<"Bags", "description">,
|
|
141
|
-
is_plugin: PrismaField<"Bags", "is_plugin">,
|
|
142
|
-
{ allowPrivilegedCharacters = false }: { allowPrivilegedCharacters?: boolean; } = {}
|
|
143
|
-
) {
|
|
144
|
-
return await this.upsertBag_PrismaPromise(bag_name, description, is_plugin, {
|
|
145
|
-
allowPrivilegedCharacters
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
upsertBag_PrismaPromise(
|
|
149
|
-
bag_name: PrismaField<"Bags", "bag_name">,
|
|
150
|
-
description: PrismaField<"Bags", "description">,
|
|
151
|
-
is_plugin: PrismaField<"Bags", "is_plugin">,
|
|
152
|
-
{ allowPrivilegedCharacters = false }: { allowPrivilegedCharacters?: boolean; } = {}
|
|
153
|
-
) {
|
|
154
|
-
|
|
155
|
-
const validationBagName = this.validateItemName(bag_name, allowPrivilegedCharacters);
|
|
156
|
-
if (validationBagName) throw validationBagName;
|
|
157
|
-
|
|
158
|
-
return this.prisma.bags.upsert({
|
|
159
|
-
where: { bag_name },
|
|
160
|
-
update: { description, is_plugin },
|
|
161
|
-
create: { bag_name, description, is_plugin },
|
|
162
|
-
select: { bag_id: true }
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* You probably want to call this inside a transaction.
|
|
168
|
-
*
|
|
169
|
-
```js
|
|
170
|
-
const tiddlersFromPath = $tw.loadTiddlersFromPath(tiddlerPath);
|
|
171
|
-
```
|
|
172
|
-
*/
|
|
173
|
-
async saveTiddlersFromPath(
|
|
174
|
-
tiddlersFromPath: { tiddlers: TiddlerFields[] }[],
|
|
175
|
-
bag_name: PrismaField<"Bags", "bag_name">
|
|
176
|
-
) {
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
saveTiddlersFromPath_PrismaArray(
|
|
180
|
-
tiddlersFromPath: { tiddlers: TiddlerFields[] }[],
|
|
181
|
-
bag_name: PrismaField<"Bags", "bag_name">
|
|
182
|
-
) {
|
|
183
|
-
return [
|
|
184
|
-
this.prisma.tiddlers.deleteMany({
|
|
185
|
-
where: { bag: { bag_name } }
|
|
186
|
-
}),
|
|
187
|
-
...tiddlersFromPath.flatMap(({ tiddlers }) =>
|
|
188
|
-
tiddlers.flatMap(tiddler =>
|
|
189
|
-
this.saveBagTiddlerFields_PrismaArray(tiddler, bag_name, null)
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
]
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/*
|
|
198
|
-
Returns {tiddler_id:}
|
|
199
|
-
*/
|
|
200
|
-
async saveBagTiddler(
|
|
201
|
-
incomingTiddlerFields: TiddlerFields,
|
|
202
|
-
bag_name: PrismaField<"Bags", "bag_name">
|
|
203
|
-
) {
|
|
204
|
-
const { title } = incomingTiddlerFields;
|
|
205
|
-
|
|
206
|
-
const existing_attachment_hash = await this.prisma.tiddlers.findFirst({
|
|
207
|
-
where: { title, bag: { bag_name } },
|
|
208
|
-
select: { attachment_hash: true }
|
|
209
|
-
}).then(e => e?.attachment_hash ?? null);
|
|
210
|
-
|
|
211
|
-
const { tiddlerFields, attachment_hash } = await this.attachService.processIncomingTiddler({
|
|
212
|
-
tiddlerFields: incomingTiddlerFields,
|
|
213
|
-
existing_attachment_hash,
|
|
214
|
-
existing_canonical_uri: existing_attachment_hash && this.attachService.makeCanonicalUri(bag_name, incomingTiddlerFields.title)
|
|
215
|
-
});
|
|
216
|
-
return this.saveBagTiddlerFields(tiddlerFields, bag_name, attachment_hash);
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
/*
|
|
220
|
-
Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store
|
|
221
|
-
Options include:
|
|
222
|
-
|
|
223
|
-
filepath - filepath to the attachment file
|
|
224
|
-
hash - string hash of the attachment file
|
|
225
|
-
type - content type of file as uploaded
|
|
226
|
-
|
|
227
|
-
Returns {tiddler_id:}
|
|
228
|
-
*/
|
|
229
|
-
async saveBagTiddlerWithAttachment(
|
|
230
|
-
incomingTiddlerFields: TiddlerFields,
|
|
231
|
-
bag_name: PrismaField<"Bags", "bag_name">,
|
|
232
|
-
options: {
|
|
233
|
-
filepath: string;
|
|
234
|
-
hash: string;
|
|
235
|
-
type: string;
|
|
236
|
-
_canonical_uri: string;
|
|
237
|
-
}
|
|
238
|
-
) {
|
|
239
|
-
const attachment_hash = await this.attachService.adoptAttachment({
|
|
240
|
-
// options.filepath, options.type, options.hash, options._canonical_uri
|
|
241
|
-
incomingFilepath: options.filepath,
|
|
242
|
-
type: options.type,
|
|
243
|
-
hash: options.hash,
|
|
244
|
-
_canonical_uri: options._canonical_uri
|
|
245
|
-
});
|
|
246
|
-
if (attachment_hash) {
|
|
247
|
-
return this.saveBagTiddlerFields(incomingTiddlerFields, bag_name, attachment_hash);
|
|
248
|
-
} else {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/*
|
|
253
|
-
Returns {tiddler_id:,bag_name:}
|
|
254
|
-
|
|
255
|
-
The critical difference here is that the tiddler gets saved to the top bag.
|
|
256
|
-
*/
|
|
257
|
-
async saveRecipeTiddler(
|
|
258
|
-
incomingTiddlerFields: TiddlerFields,
|
|
259
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">
|
|
260
|
-
) {
|
|
261
|
-
const { title } = incomingTiddlerFields;
|
|
262
|
-
const currentBag = await this.getRecipeBagWithTiddler({ recipe_name, title });
|
|
263
|
-
const existing_attachment_hash = currentBag && await this.prisma.tiddlers.findFirst({
|
|
264
|
-
where: { title, bag: { bag_name: currentBag.bag.bag_name } },
|
|
265
|
-
select: { attachment_hash: true }
|
|
266
|
-
}).then(e => e?.attachment_hash ?? null);
|
|
267
|
-
|
|
268
|
-
const { tiddlerFields, attachment_hash } = await this.attachService.processIncomingTiddler({
|
|
269
|
-
existing_attachment_hash,
|
|
270
|
-
existing_canonical_uri: incomingTiddlerFields._canonical_uri,
|
|
271
|
-
tiddlerFields: incomingTiddlerFields
|
|
272
|
-
});
|
|
273
|
-
return this.saveRecipeTiddlerFields(tiddlerFields, recipe_name, attachment_hash);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/*
|
|
277
|
-
returns {tiddler_id:,tiddler:}
|
|
278
|
-
*/
|
|
279
|
-
async getBagTiddler(options: {
|
|
280
|
-
title: PrismaField<"Tiddlers", "title">;
|
|
281
|
-
bag_name?: PrismaField<"Bags", "bag_name">;
|
|
282
|
-
bag_id?: PrismaField<"Bags", "bag_id">;
|
|
283
|
-
}) {
|
|
284
|
-
const { title, bag_name, bag_id } = options;
|
|
285
|
-
ok(bag_name || bag_id, "bag_name or bag_id must be provided");
|
|
286
|
-
|
|
287
|
-
const tiddler = await this.prisma.tiddlers.findFirst({
|
|
288
|
-
where: bag_name
|
|
289
|
-
? { title, bag: { bag_name }, is_deleted: false }
|
|
290
|
-
: { title, bag_id, is_deleted: false },
|
|
291
|
-
select: { tiddler_id: true }
|
|
292
|
-
});
|
|
293
|
-
if (!tiddler) return null;
|
|
294
|
-
|
|
295
|
-
var tiddlerInfo = await this.getTiddlerInfo(tiddler.tiddler_id);
|
|
296
|
-
if (!tiddlerInfo) return null;
|
|
297
|
-
|
|
298
|
-
// if (tiddlerInfo.is_plugin) {
|
|
299
|
-
// const pluginBag = await this.prisma.bags.findUnique({
|
|
300
|
-
// where: { bag_name: tiddlerInfo.bag_name },
|
|
301
|
-
// include: { tiddlers: { include: { fields: true } } }
|
|
302
|
-
// });
|
|
303
|
-
// }
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
...tiddlerInfo,
|
|
307
|
-
tiddler: this.attachService.processOutgoingTiddler(tiddlerInfo)
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async getRecipeTiddler(
|
|
313
|
-
title: PrismaField<"Tiddlers", "title">,
|
|
314
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">
|
|
315
|
-
) {
|
|
316
|
-
|
|
317
|
-
const recipe_bag = await this.getRecipeBagWithTiddler({ recipe_name, title });
|
|
318
|
-
|
|
319
|
-
if (!recipe_bag) return null;
|
|
320
|
-
|
|
321
|
-
return await this.getBagTiddler({ title, bag_id: recipe_bag.bag_id });
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
async getRecipeBagWithTiddler({ recipe_name, title }: { recipe_name: string; title: string; }) {
|
|
327
|
-
|
|
328
|
-
return await this.prisma.recipe_bags.findFirst({
|
|
329
|
-
include: { bag: true, recipe: true },
|
|
330
|
-
where: {
|
|
331
|
-
recipe: { recipe_name },
|
|
332
|
-
bag: { tiddlers: { some: { title, is_deleted: false } } }
|
|
333
|
-
},
|
|
334
|
-
orderBy: { position: "asc" }
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async getRecipeWritableBag(
|
|
339
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">
|
|
340
|
-
) {
|
|
341
|
-
const recipe_bag = await this.prisma.recipe_bags.findFirst({
|
|
342
|
-
select: { bag: true },
|
|
343
|
-
where: { recipe: { recipe_name }, position: 0 }
|
|
344
|
-
});
|
|
345
|
-
if (!recipe_bag) return null;
|
|
346
|
-
return recipe_bag.bag;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
async getTiddlerInfo(
|
|
351
|
-
tiddler_id: PrismaField<"Tiddlers", "tiddler_id">
|
|
352
|
-
) {
|
|
353
|
-
|
|
354
|
-
const tiddler = await this.prisma.tiddlers.findUnique({
|
|
355
|
-
where: { tiddler_id, is_deleted: false },
|
|
356
|
-
include: { fields: true, bag: true }
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
if (!tiddler) return null;
|
|
360
|
-
|
|
361
|
-
if (tiddler.bag.is_plugin && tiddler.title as string !== tiddler.bag.bag_name as string)
|
|
362
|
-
throw new UserError("Can only get tiddler info for a plugin bag itself");
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
bag_name: tiddler.bag.bag_name,
|
|
366
|
-
is_plugin_bag: tiddler.bag.is_plugin,
|
|
367
|
-
tiddler_id: tiddler.tiddler_id,
|
|
368
|
-
attachment_hash: tiddler.attachment_hash,
|
|
369
|
-
tiddler: Object.fromEntries([
|
|
370
|
-
...tiddler.fields.map(e => [e.field_name, e.field_value] as const),
|
|
371
|
-
["title", tiddler.title]
|
|
372
|
-
]) as TiddlerFields
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
/*
|
|
376
|
-
Get an attachment ready to stream. Returns null if there is an error or:
|
|
377
|
-
tiddler_id: revision of tiddler
|
|
378
|
-
stream: stream of file
|
|
379
|
-
type: type of file
|
|
380
|
-
Returns {tiddler_id:,bag_name:}
|
|
381
|
-
*/
|
|
382
|
-
async getBagTiddlerStream(
|
|
383
|
-
title: PrismaField<"Tiddlers", "title">,
|
|
384
|
-
bag_name: PrismaField<"Bags", "bag_name">
|
|
385
|
-
) {
|
|
386
|
-
const tiddlerInfo = await this.getBagTiddler({ title, bag_name });
|
|
387
|
-
if (!tiddlerInfo) return null;
|
|
388
|
-
|
|
389
|
-
if (tiddlerInfo.attachment_hash) {
|
|
390
|
-
return {
|
|
391
|
-
...await this.attachService.getAttachmentStream(tiddlerInfo.attachment_hash) ?? {},
|
|
392
|
-
tiddler_id: tiddlerInfo.tiddler_id,
|
|
393
|
-
bag_name: bag_name
|
|
394
|
-
};
|
|
395
|
-
} else {
|
|
396
|
-
const { Readable } = require('stream');
|
|
397
|
-
const stream = new Readable();
|
|
398
|
-
stream._read = () => {
|
|
399
|
-
// Push data
|
|
400
|
-
const type = tiddlerInfo.tiddler.type || "text/plain";
|
|
401
|
-
stream.push(tiddlerInfo.tiddler.text || "", (this.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding);
|
|
402
|
-
// Push null to indicate the end of the stream
|
|
403
|
-
stream.push(null);
|
|
404
|
-
};
|
|
405
|
-
return {
|
|
406
|
-
tiddler_id: tiddlerInfo.tiddler_id,
|
|
407
|
-
bag_name: bag_name,
|
|
408
|
-
stream: stream,
|
|
409
|
-
type: tiddlerInfo.tiddler.type || "text/plain"
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
Returns {tiddler_id:,bag_name:} or null if the recipe is empty
|
|
417
|
-
*/
|
|
418
|
-
async saveRecipeTiddlerFields(
|
|
419
|
-
tiddlerFields: TiddlerFields,
|
|
420
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">,
|
|
421
|
-
attachment_hash: PrismaField<"Tiddlers", "attachment_hash">
|
|
422
|
-
) {
|
|
423
|
-
|
|
424
|
-
const recipe = await this.prisma.recipes.findUnique({
|
|
425
|
-
where: { recipe_name },
|
|
426
|
-
include: {
|
|
427
|
-
recipe_bags: {
|
|
428
|
-
// we're saving to the top most bag
|
|
429
|
-
where: { position: 0 },
|
|
430
|
-
select: { bag: { select: { bag_id: true, bag_name: true } } },
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
if (!recipe) throw new UserError("Recipe not found");
|
|
436
|
-
|
|
437
|
-
const bag_name = recipe.recipe_bags[0]?.bag.bag_name;
|
|
438
|
-
|
|
439
|
-
if (!bag_name) throw new UserError("Recipe has no bag at position 0");
|
|
440
|
-
|
|
441
|
-
// Save the tiddler to the specified bag
|
|
442
|
-
var { tiddler_id } = await this.saveBagTiddlerFields(tiddlerFields, bag_name, attachment_hash);
|
|
443
|
-
|
|
444
|
-
return { tiddler_id, bag_name };
|
|
445
|
-
// // Find the topmost bag in the recipe
|
|
446
|
-
// var row = this.engine.runStatementGet(`
|
|
447
|
-
// SELECT b.bag_name
|
|
448
|
-
// FROM bags AS b
|
|
449
|
-
// JOIN (
|
|
450
|
-
// SELECT rb.bag_id
|
|
451
|
-
// FROM recipe_bags AS rb
|
|
452
|
-
// WHERE rb.recipe_id = (
|
|
453
|
-
// SELECT recipe_id
|
|
454
|
-
// FROM recipes
|
|
455
|
-
// WHERE recipe_name = $recipe_name
|
|
456
|
-
// )
|
|
457
|
-
// ORDER BY rb.position DESC
|
|
458
|
-
// LIMIT 1
|
|
459
|
-
// ) AS selected_bag
|
|
460
|
-
// ON b.bag_id = selected_bag.bag_id
|
|
461
|
-
// `, {
|
|
462
|
-
// $recipe_name: recipe_name
|
|
463
|
-
// });
|
|
464
|
-
// if (!row) {
|
|
465
|
-
// return null;
|
|
466
|
-
// }
|
|
467
|
-
// // Save the tiddler to the topmost bag
|
|
468
|
-
// var info = this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_hash);
|
|
469
|
-
// return {
|
|
470
|
-
// tiddler_id: info.tiddler_id,
|
|
471
|
-
// bag_name: row.bag_name
|
|
472
|
-
// };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async saveBagTiddlerFields(
|
|
476
|
-
tiddlerFields: TiddlerFields,
|
|
477
|
-
bag_name: PrismaField<"Bags", "bag_name">,
|
|
478
|
-
attachment_hash: PrismaField<"Tiddlers", "attachment_hash">
|
|
479
|
-
) {
|
|
480
|
-
const [deletion, creation] = this.saveBagTiddlerFields_PrismaArray(tiddlerFields, bag_name, attachment_hash);
|
|
481
|
-
await deletion;
|
|
482
|
-
return await creation;
|
|
483
|
-
}
|
|
484
|
-
saveBagTiddlerFields_PrismaArray(
|
|
485
|
-
tiddlerFields: TiddlerFields,
|
|
486
|
-
bag_name: PrismaField<"Bags", "bag_name">,
|
|
487
|
-
attachment_hash: PrismaField<"Tiddlers", "attachment_hash">
|
|
488
|
-
) {
|
|
489
|
-
|
|
490
|
-
const { title } = tiddlerFields;
|
|
491
|
-
if (!title) {
|
|
492
|
-
console.error(tiddlerFields);
|
|
493
|
-
throw new Error("Tiddler must have a title");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const deletion = this.prisma.tiddlers.deleteMany({
|
|
497
|
-
where: { bag: { bag_name }, title }
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
const encodeTiddlerFields = ([field_name, field_value]: [string, any]) => {
|
|
501
|
-
const fieldModule = this.fieldModules[field_name];
|
|
502
|
-
if (typeof field_value === "string") return [field_name, field_value];
|
|
503
|
-
if (fieldModule && fieldModule.stringify) field_value = fieldModule.stringify(field_value);
|
|
504
|
-
if (typeof field_value === "number") field_value = field_value.toString();
|
|
505
|
-
if (typeof field_value === "boolean") field_value = field_value ? "true" : "false";
|
|
506
|
-
if (typeof field_value === "undefined" || field_value === null) field_value = "";
|
|
507
|
-
return [field_name, field_value];
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const creation = this.prisma.tiddlers.create({
|
|
511
|
-
data: {
|
|
512
|
-
bag: { connect: { bag_name } },
|
|
513
|
-
title,
|
|
514
|
-
is_deleted: false,
|
|
515
|
-
attachment_hash: attachment_hash || null,
|
|
516
|
-
fields: {
|
|
517
|
-
create: Object.entries(tiddlerFields)
|
|
518
|
-
.map(encodeTiddlerFields)
|
|
519
|
-
.map(([field_name, field_value]) => ({ field_name, field_value }))
|
|
520
|
-
}
|
|
521
|
-
},
|
|
522
|
-
select: {
|
|
523
|
-
tiddler_id: true
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
return [deletion, creation] as const;
|
|
528
|
-
|
|
529
|
-
// attachment_hash = attachment_hash || null;
|
|
530
|
-
// // Update the tiddlers table
|
|
531
|
-
// var info = this.engine.runStatement(`
|
|
532
|
-
// INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_hash)
|
|
533
|
-
// VALUES (
|
|
534
|
-
// (SELECT bag_id FROM bags WHERE bag_name = $bag_name),
|
|
535
|
-
// $title,
|
|
536
|
-
// FALSE,
|
|
537
|
-
// $attachment_hash
|
|
538
|
-
// )
|
|
539
|
-
// `, {
|
|
540
|
-
// $title: tiddlerFields.title,
|
|
541
|
-
// $attachment_hash: attachment_hash,
|
|
542
|
-
// $bag_name: bag_name
|
|
543
|
-
// });
|
|
544
|
-
// // Update the fields table
|
|
545
|
-
// this.engine.runStatement(`
|
|
546
|
-
// INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value)
|
|
547
|
-
// SELECT
|
|
548
|
-
// t.tiddler_id,
|
|
549
|
-
// json_each.key AS field_name,
|
|
550
|
-
// json_each.value AS field_value
|
|
551
|
-
// FROM (
|
|
552
|
-
// SELECT tiddler_id
|
|
553
|
-
// FROM tiddlers
|
|
554
|
-
// WHERE bag_id = (
|
|
555
|
-
// SELECT bag_id
|
|
556
|
-
// FROM bags
|
|
557
|
-
// WHERE bag_name = $bag_name
|
|
558
|
-
// ) AND title = $title
|
|
559
|
-
// ) AS t
|
|
560
|
-
// JOIN json_each($field_values) AS json_each
|
|
561
|
-
// `, {
|
|
562
|
-
// $title: tiddlerFields.title,
|
|
563
|
-
// $bag_name: bag_name,
|
|
564
|
-
// $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined }))
|
|
565
|
-
// });
|
|
566
|
-
// return {
|
|
567
|
-
// tiddler_id: info.lastInsertRowid
|
|
568
|
-
// };
|
|
569
|
-
}
|
|
570
|
-
/*
|
|
571
|
-
Returns {tiddler_id:,bag_name:}
|
|
572
|
-
*/
|
|
573
|
-
async deleteRecipeTiddler(
|
|
574
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">,
|
|
575
|
-
title: PrismaField<"Tiddlers", "title">
|
|
576
|
-
) {
|
|
577
|
-
|
|
578
|
-
const currentBag = await this.getRecipeBagWithTiddler({ recipe_name, title });
|
|
579
|
-
if (!currentBag)
|
|
580
|
-
throw new UserError("Tiddler not found in recipe");
|
|
581
|
-
if (currentBag.position !== 0)
|
|
582
|
-
throw new UserError("Cannot delete tiddler from non-top bag");
|
|
583
|
-
return await this.deleteBagTiddler(title, currentBag.bag.bag_name);
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
Returns {tiddler_id:} of the delete marker
|
|
587
|
-
*/
|
|
588
|
-
async deleteBagTiddler(
|
|
589
|
-
title: PrismaField<"Tiddlers", "title">,
|
|
590
|
-
bag_name: PrismaField<"Bags", "bag_name">
|
|
591
|
-
) {
|
|
592
|
-
await this.prisma.tiddlers.deleteMany({
|
|
593
|
-
where: { title, bag: { bag_name } },
|
|
594
|
-
});
|
|
595
|
-
const { tiddler_id } = await this.prisma.tiddlers.create({
|
|
596
|
-
data: {
|
|
597
|
-
title,
|
|
598
|
-
bag: { connect: { bag_name } },
|
|
599
|
-
is_deleted: true,
|
|
600
|
-
attachment_hash: null,
|
|
601
|
-
},
|
|
602
|
-
select: {
|
|
603
|
-
tiddler_id: true
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
return { bag_name, tiddler_id }
|
|
608
|
-
|
|
609
|
-
// // Delete the fields of this tiddler
|
|
610
|
-
// this.engine.runStatement(`
|
|
611
|
-
// DELETE FROM fields
|
|
612
|
-
// WHERE tiddler_id IN (
|
|
613
|
-
// SELECT t.tiddler_id
|
|
614
|
-
// FROM tiddlers AS t
|
|
615
|
-
// INNER JOIN bags AS b ON t.bag_id = b.bag_id
|
|
616
|
-
// WHERE b.bag_name = $bag_name AND t.title = $title
|
|
617
|
-
// )
|
|
618
|
-
// `, {
|
|
619
|
-
// $title: title,
|
|
620
|
-
// $bag_name: bag_name
|
|
621
|
-
// });
|
|
622
|
-
// // Mark the tiddler itself as deleted
|
|
623
|
-
// const rowDeleteMarker = this.engine.runStatement(`
|
|
624
|
-
// INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_hash)
|
|
625
|
-
// VALUES (
|
|
626
|
-
// (SELECT bag_id FROM bags WHERE bag_name = $bag_name),
|
|
627
|
-
// $title,
|
|
628
|
-
// TRUE,
|
|
629
|
-
// NULL
|
|
630
|
-
// )
|
|
631
|
-
// `, {
|
|
632
|
-
// $title: title,
|
|
633
|
-
// $bag_name: bag_name
|
|
634
|
-
// });
|
|
635
|
-
// return { tiddler_id: rowDeleteMarker.lastInsertRowid };
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async getRecipeTiddlersByBag(
|
|
639
|
-
recipe_name: PrismaField<"Recipes", "recipe_name">,
|
|
640
|
-
options: {
|
|
641
|
-
// how do you limit a list of unique titles?
|
|
642
|
-
// limit?: number,
|
|
643
|
-
last_known_tiddler_id?: number,
|
|
644
|
-
include_deleted?: boolean,
|
|
645
|
-
} = {}
|
|
646
|
-
) {
|
|
647
|
-
// In prisma it's easy to get the top bag for a specific title.
|
|
648
|
-
// To get all titles we basically have to get all bags and manually find the top one.
|
|
649
|
-
const lastid = options.last_known_tiddler_id;
|
|
650
|
-
const withDeleted = options.include_deleted;
|
|
651
|
-
const bags = await this.prisma.recipe_bags.findMany({
|
|
652
|
-
where: { recipe: { recipe_name } },
|
|
653
|
-
select: {
|
|
654
|
-
position: true,
|
|
655
|
-
bag: {
|
|
656
|
-
select: {
|
|
657
|
-
bag_id: true,
|
|
658
|
-
bag_name: true,
|
|
659
|
-
is_plugin: true,
|
|
660
|
-
tiddlers: {
|
|
661
|
-
select: {
|
|
662
|
-
title: true,
|
|
663
|
-
tiddler_id: true,
|
|
664
|
-
is_deleted: true,
|
|
665
|
-
},
|
|
666
|
-
where: {
|
|
667
|
-
is_deleted: withDeleted ? undefined : false,
|
|
668
|
-
tiddler_id: lastid ? { gt: lastid } : undefined
|
|
669
|
-
},
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return bags.map(e => ({
|
|
678
|
-
bag_id: e.bag.bag_id,
|
|
679
|
-
bag_name: e.bag.bag_name,
|
|
680
|
-
is_plugin: e.bag.is_plugin,
|
|
681
|
-
position: e.position,
|
|
682
|
-
tiddlers: e.bag.tiddlers
|
|
683
|
-
}));
|
|
684
|
-
|
|
685
|
-
// // Get the recipe ID
|
|
686
|
-
// const rowsCheckRecipe = this.engine.runStatementGet(`
|
|
687
|
-
// SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name
|
|
688
|
-
// `, {
|
|
689
|
-
// $recipe_name: recipe_name
|
|
690
|
-
// });
|
|
691
|
-
// if (!rowsCheckRecipe) {
|
|
692
|
-
// return null;
|
|
693
|
-
// }
|
|
694
|
-
// const recipe_id = rowsCheckRecipe.recipe_id;
|
|
695
|
-
// // Compose the query to get the tiddlers
|
|
696
|
-
// const params = {
|
|
697
|
-
// $recipe_id: recipe_id
|
|
698
|
-
// };
|
|
699
|
-
// if (options.limit) {
|
|
700
|
-
// params.$limit = options.limit.toString();
|
|
701
|
-
// }
|
|
702
|
-
// if (options.last_known_tiddler_id) {
|
|
703
|
-
// params.$last_known_tiddler_id = options.last_known_tiddler_id;
|
|
704
|
-
// }
|
|
705
|
-
// const rows = this.engine.runStatementGetAll(`
|
|
706
|
-
// SELECT title, tiddler_id, is_deleted, bag_name
|
|
707
|
-
// FROM (
|
|
708
|
-
// SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position
|
|
709
|
-
// FROM bags AS b
|
|
710
|
-
// INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id
|
|
711
|
-
// INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id
|
|
712
|
-
// WHERE rb.recipe_id = $recipe_id
|
|
713
|
-
// ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"}
|
|
714
|
-
// ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""}
|
|
715
|
-
// GROUP BY t.title
|
|
716
|
-
// ORDER BY t.title, tiddler_id DESC
|
|
717
|
-
// ${options.limit ? "LIMIT $limit" : ""}
|
|
718
|
-
// )
|
|
719
|
-
// `, params);
|
|
720
|
-
// return rows;
|
|
721
|
-
}
|
|
722
|
-
}
|