Skip to content

Commit 17877be

Browse files
committed
add admin validation
1 parent f1ed053 commit 17877be

6 files changed

Lines changed: 51 additions & 85 deletions

File tree

ARCHITECTURE.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Depending on server and template configuration, plugin assets and store data may
5454

5555
The admin surface manages wikis, templates, bags, users, and roles. In this model, a wiki is the editable public resource, a template is the reusable shared definition, and a bag is the underlying storage unit.
5656

57-
Saving a wiki or template changes the configuration that future runtime requests use. Template changes affect every wiki that depends on that template. Bag changes affect where titles are stored and resolved.
57+
Saving a wiki or template changes the configuration that future runtime requests use. Template changes affect every wiki that depends on that template.
5858

5959
Plugin entries are exposed through the admin load path so they can be selected and inspected, but this path does not treat plugins as editable database rows in the same way as wikis, templates, bags, users, and roles.
6060

@@ -122,6 +122,5 @@ These are the hooks that run on startup. The listen command starts the server an
122122

123123
## Todo
124124

125-
- server input validation: all the pieces are there, I just need to wire it up.
126-
- make commands easier to extend, probably just by making it easier to specify a list of commands via javascript.
127-
- clean up my spaghetti code into nice straight rows of pasta.
125+
- improve the small screen form factor for the admin UI
126+
- make sure all the descriptions and labels make sense

packages/admin-vanilla/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ class PerTabStoreImpl implements PerTabStore {
323323
});
324324

325325
const item = await this.storage.read(tabId, recordId).catch((error) => {
326+
console.log(error);
326327
this.patchState({
327328
storageError: error instanceof Error ? error.message : "Failed to load record details.",
328329
});
@@ -426,6 +427,7 @@ class PerTabStoreImpl implements PerTabStore {
426427
snapshot.tabId,
427428
snapshot.draft,
428429
).catch((error) => {
430+
console.log(error);
429431
this.patchState({
430432
storageError: error instanceof Error ? error.message : "Failed to save record.",
431433
});

packages/admin-vanilla/src/definition/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ function syncWikiRecord(draft: DataStore["wikis"][number], data: DataStore) {
283283

284284
const mergedReadonlyBags = uniqueLines([...wikiReadonlyBags, ...templateReadonlyBags].map(e => e));
285285
const mergedPlugins = buildEffectivePluginSet({
286-
previousEffectivePlugins: (draft as WikiAdminRecord).effectivePluginSet,
286+
previousEffectivePlugins: draft.effectivePluginSet,
287287
templatePlugins,
288288
wikiPlugins,
289289
corePluginsEnabled: !!templateRecord?.requiredPluginsEnabled,

packages/admin-vanilla/src/definition/tabs.ts

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -55,41 +55,44 @@ export type FieldType =
5555
| "metadata-table";
5656

5757

58+
export class IdString extends String {
59+
static cast(val: IdString): string { return val.toString(); }
60+
static prefix = "IdString____";
61+
name = "IdString" as const;
62+
constructor(value: string) { super(value); }
63+
toJSON() { return IdString.prefix + this.valueOf(); }
64+
}
5865

59-
const stringLikeFieldShape = z.union([z.string(), z.instanceof(String)]);
60-
const stringListFieldShape = z.array(stringLikeFieldShape);
61-
const writablePrefixRowFieldShape = z.object({
62-
prefix: z.string(),
63-
bagName: stringLikeFieldShape,
64-
});
65-
const permissionRowFieldShape = z.object({
66-
role: stringLikeFieldShape,
67-
level: z.string(),
68-
});
69-
66+
const stringLikeFieldShape = z.union([z.string(), z.instanceof(IdString)]);
7067

7168
export const fieldTypeZodShapes = {
7269
"template-type": z.enum(["simpleV1"]),
7370
"string": z.string(),
7471
"text": z.string(),
7572
"enter-password": z.string(),
7673
"confirm-password": z.string(),
77-
"search": z.union([stringLikeFieldShape, z.null()]),
74+
"search": z.string(),
7875
"structured-preview": z.string(),
79-
"table": stringListFieldShape,
80-
"permission-table": z.array(permissionRowFieldShape),
76+
"table": z.string().array(),
77+
"permission-table": z.array(z.object({
78+
role: z.string(),
79+
level: z.string(),
80+
})),
8181
"validation-report": z.string(),
8282
"resolver-preview": z.string(),
83-
"search-multiselect": stringListFieldShape,
84-
"prefix-table": z.array(writablePrefixRowFieldShape),
85-
"parameter-list": stringListFieldShape,
86-
"relationship-table": stringListFieldShape,
87-
"summary-list": stringListFieldShape,
83+
"search-multiselect": z.string().array(),
84+
"prefix-table": z.array(z.object({
85+
prefix: z.string(),
86+
bagName: z.string(),
87+
})),
88+
"parameter-list": z.string().array(),
89+
"relationship-table": z.string().array(),
90+
"summary-list": z.string().array(),
8891
"number": z.string(),
89-
"activity-feed": stringListFieldShape,
92+
"activity-feed": z.string().array(),
9093
"version": z.string(),
9194
"select": z.boolean(),
92-
"metadata-table": stringListFieldShape,
95+
"metadata-table": z.string().array(),
9396
} satisfies Record<FieldType, any>;
9497

9598
export type FieldTypeCreateValue<T extends FieldType = FieldType> = z.infer<(typeof fieldTypeZodShapes)[T]>;
@@ -100,7 +103,7 @@ export const fieldTypeCreateFactories = {
100103
"text": () => "",
101104
"enter-password": () => "",
102105
"confirm-password": () => "",
103-
"search": () => null,
106+
"search": () => "",
104107
"structured-preview": () => "",
105108
"table": () => [],
106109
"permission-table": () => [],
@@ -773,13 +776,6 @@ export interface KeyValueRow {
773776
}
774777

775778

776-
export class IdString extends String {
777-
static cast(val: IdString): string { return val.toString(); }
778-
static prefix = "IdString____";
779-
name = "IdString" as const;
780-
constructor(value: string) { super(value); }
781-
toJSON() { return IdString.prefix + this.valueOf(); }
782-
}
783779
// export class string extends String {
784780
// static cast(val: string): string { return val; }
785781
// static prefix = "KeyString____";
@@ -806,12 +802,7 @@ export function buildTabZodObject<T extends TabId>(tabId: T, filter?: TabZodObje
806802
const tab = tabs[tabId];
807803
const fieldEntries = tab.fields
808804
.filter((field) => shouldIncludeFieldInTabZodObject(field.mode, filter))
809-
.map((field) => [
810-
field.key,
811-
// KeyFields[tabId] === field.key
812-
// ? z.instanceof(KeyString)
813-
fieldTypeZodShapes[field.type]
814-
] as const);
805+
.map((field) => [field.key, fieldTypeZodShapes[field.type]] as const);
815806

816807
return z.object({
817808
id: stringLikeFieldShape,

packages/mws/src/new-managers/TabDataAdapter.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
SendError,
1515
ServerRequest,
1616
Z2,
17+
zod,
1718
zodRoute
1819
} from "@tiddlywiki/server";
1920
import {
@@ -77,11 +78,6 @@ export type {
7778
UpsertUserInput,
7879
} from "./wiki-contract";
7980
export {
80-
importBags,
81-
importRecipes,
82-
importRoles,
83-
importTemplates,
84-
importUsers,
8581
type ImportedBagRows,
8682
type ImportedRecipeRows,
8783
type ImportedRoleRows,
@@ -541,14 +537,14 @@ export const AdminSave = zodRoute({
541537
state.data = JSON.parse(JSON.stringify(state.data), (key: any, val: any) => {
542538
if (typeof val === "string" && val.startsWith(IdString.prefix))
543539
return new IdString(val.slice(IdString.prefix.length));
544-
if (typeof val === "string" && val.startsWith("KeyString____"))
545-
return val.slice("KeyString____".length);
540+
// if (typeof val === "string" && val.startsWith("KeyString____"))
541+
// return val.slice("KeyString____".length);
546542
return val;
547543
});
548544

549545
checkData(state, () => buildTabZodObject(state.pathParams.tab, "DataSave"), new Error());
550546

551-
return await state.$transaction(async prisma => {
547+
const res = await state.$transaction(async prisma => {
552548
const { pathParams: { op, tab }, data } = state;
553549
if (op !== "save") throw new Error(`Unsupported admin operation: ${op}`);
554550
switch (tab) {
@@ -574,6 +570,10 @@ export const AdminSave = zodRoute({
574570
}
575571
}
576572
});
573+
574+
const { success, data: res2, error } = buildTabZodObject(state.pathParams.tab, "DataStore").safeParse(res);
575+
if (!success) console.log("Response validation: ", error);
576+
return res2;
577577
}
578578
});
579579

@@ -586,7 +586,7 @@ export const AdminLoad = zodRoute({
586586
inner: async (state) => {
587587
// access is checked in each list
588588
state.asserted = state.user.isLoggedIn;
589-
return await state.$transaction(async prisma => {
589+
const res = await state.$transaction(async prisma => {
590590
const roles = await new RoleImportWriter(prisma, false).getIdMapper();
591591
return {
592592
wikis: await new RecipeDataAdapter(state.user).getList(prisma, roles),
@@ -602,5 +602,15 @@ export const AdminLoad = zodRoute({
602602
users: await new UserDataAdapter(state.user).getList(prisma, roles),
603603
};
604604
});
605+
const { success, data, error } = zod.object({
606+
wikis: buildTabZodObject("wikis", "DataStore").array(),
607+
templates: buildTabZodObject("templates", "DataStore").array(),
608+
bags: buildTabZodObject("bags", "DataStore").array(),
609+
plugins: buildTabZodObject("plugins", "DataStore").array(),
610+
roles: buildTabZodObject("roles", "DataStore").array(),
611+
users: buildTabZodObject("users", "DataStore").array(),
612+
}).safeParse(res);
613+
if (!success) throw error;
614+
return data;
605615
}
606616
});

packages/mws/src/new-managers/TabImportWriter.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ export class RecipeImportWriter extends PerClassImportWriter<"recipe"> {
398398
this.assertPermissions();
399399
const upserter = async (recipe: UpsertRecipeInput, compiledAt: Date) => {
400400
recipe.compiledBags.sort((a, b) => a.priority - b.priority);
401-
console.log(recipe.compiledBags);
402401
return await this.tx.recipe.upsert({
403402
where: { slug: recipe.slug },
404403
update: {
@@ -511,41 +510,6 @@ export class RecipeImportWriter extends PerClassImportWriter<"recipe"> {
511510
}
512511
// #region OTHER
513512

514-
export function importRoles(prisma: PrismaEngineClient, roles: UpsertRoleInput[], initStore = false) {
515-
return prisma.$transaction(async (tx) => {
516-
const importer = new RoleImportWriter(tx, initStore);
517-
return importer.upsert(roles);
518-
});
519-
}
520-
521-
export function importUsers(prisma: PrismaEngineClient, users: UpsertUserInput[], initStore = false) {
522-
return prisma.$transaction(async (tx) => {
523-
const importer = new UserImportWriter(tx, initStore);
524-
return importer.upsert(users);
525-
});
526-
}
527-
528-
export function importBags(prisma: PrismaEngineClient, bags: UpsertBagInput[], initStore = false) {
529-
return prisma.$transaction(async (tx) => {
530-
const importer = new BagImportWriter(tx, initStore);
531-
return importer.upsert(bags);
532-
});
533-
}
534-
535-
export function importTemplates(prisma: PrismaEngineClient, templates: UpsertTemplateInput[], initStore = false) {
536-
return prisma.$transaction(async (tx) => {
537-
const importer = new TemplateImportWriter(tx, initStore);
538-
return importer.upsert(templates);
539-
});
540-
}
541-
542-
export function importRecipes(prisma: PrismaEngineClient, recipes: UpsertRecipeInput[], initStore = false) {
543-
return prisma.$transaction(async (tx) => {
544-
const importer = new RecipeImportWriter(tx, initStore);
545-
return importer.upsert(recipes);
546-
});
547-
}
548-
549513
export function toMappingRows(obj: Record<string, string>) {
550514
return Object.entries(obj).map(([prefix, bagName]) => ({ prefix, bagName }));
551515
}

0 commit comments

Comments
 (0)