Skip to content

Commit 85256a2

Browse files
committed
save
1 parent 71c5787 commit 85256a2

7 files changed

Lines changed: 322 additions & 287 deletions

File tree

packages/admin-mdui/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
"@types/react": "^19.2.2",
1313
"esbuild-plugin-copy": "^2.1.1",
1414
"events": "^3.3.0",
15-
1615
"luxon": "^3.7.1",
17-
"mdui": "file:mdui-2.1.4.tgz",
1816
"normalize.css": "^8.0.1",
1917
"react": "^19.2.0",
2018
"replicache": "^15.3.0",

packages/mws/src/new-commands/load-wiki-folder.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export class Command extends BaseCommand<[string], {
114114
switch (template.type) {
115115
case "simpleV1": {
116116
const bag_id = await saveBagRow(prisma, {
117-
id: "",
117+
id: "", // a blank id will use the name if it exists
118118
name: bagName,
119119
description: bagDescription,
120120
permissions: ownerRoles.map(role => ({ level: "C_admin", role: role })),
@@ -125,7 +125,7 @@ export class Command extends BaseCommand<[string], {
125125
}
126126

127127
const recipe_id = await saveWikiRow(prisma, {
128-
id: "",
128+
id: "", // a blank id will use the slug if it exists
129129
slug: recipeName,
130130
templateRef: { id: template.id, name: template.name },
131131
displayName: recipeName,
@@ -136,7 +136,12 @@ export class Command extends BaseCommand<[string], {
136136
recipePermissions: ownerRoles.map(role => ({ level: "B_write", role })),
137137
});
138138

139-
await saveLoadedTiddlers(prisma, recipe_id, bag_id, tiddlers);
139+
const existingTitles = Array.from(await prisma.tiddler.findMany({
140+
where: { bag_id },
141+
select: { title: true }
142+
}), e => e.title);
143+
144+
await saveLoadedTiddlers(prisma, recipe_id, bag_id, tiddlers, existingTitles);
140145
}
141146
}
142147

@@ -155,23 +160,19 @@ async function saveLoadedTiddlers(
155160
recipe_id: string,
156161
bag_id: string,
157162
tiddlers: TiddlerFields[],
163+
existingTitles: string[],
158164
) {
165+
const newTitles = new Set(tiddlers.map(e => e.title));
166+
const store = new WikiStore(tx);
167+
for (const title of existingTitles) {
168+
if (!newTitles.has(title)) {
169+
await store.deleteTiddler({ recipe_id, bag_id, title, });
170+
}
171+
}
159172
for (const fields of tiddlers) {
160173
const title = fields.title;
161-
if (!title) {
162-
throw new Error("Tiddler must have a title");
163-
}
164-
const store = new WikiStore(tx);
165-
await store.deleteTiddler({
166-
recipe_id,
167-
bag_id,
168-
title,
169-
});
170-
await store.saveTiddler({
171-
recipe_id,
172-
bag_id,
173-
fields,
174-
});
174+
if (!title) throw new Error("Tiddler must have a title");
175+
await store.saveTiddler({ recipe_id, bag_id, fields, });
175176
}
176177
}
177178

packages/mws/src/new-managers/NEW-DESIGN-V2.md

Lines changed: 247 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,15 +359,17 @@ Delete is valid even when lower bags still contain the title. In that case the t
359359

360360
### Batch
361361

362-
Batch operations are per-title and non-atomic.
362+
Batch operations succeed or fail as a whole.
363363

364-
Each item returns its own result object.
364+
The request is treated as one operation at the API contract level. If any item in the batch is invalid or denied, the batch fails rather than returning a mixed per-item result set.
365+
366+
Atomicity of the underlying writes is not yet defined.
365367

366368
Reasons:
367369

368-
- write denial is a routine outcome, not a system failure
369-
- titles in a batch may route to different bags
370-
- partial success is expected in collaborative use
370+
- callers should not have to reconcile mixed success and failure outcomes inside one batch response
371+
- the server contract is simpler if batch validation and authorization failures abort the batch response
372+
- routing may still differ per title, but that no longer implies partial success is part of the public contract
371373

372374
## Change Log and Sync
373375

@@ -489,4 +491,243 @@ The implementation should keep a strict separation between:
489491
- compiled state: recipe bags and recipe plugins
490492
- resolved state: tiddler reads and write targets for a given title and principal
491493

492-
If the code preserves that split, the model remains understandable as new wiki types are added.
494+
If the code preserves that split, the model remains understandable as new wiki types are added.
495+
496+
## Structure Lockdown
497+
498+
The current code is close enough to a stable shape that the next work should not be more exploratory refactoring. It should be convergence work against a fixed set of module responsibilities.
499+
500+
This section defines the intended ownership boundaries.
501+
502+
### Module Responsibilities
503+
504+
#### `wiki-contract.ts`
505+
506+
This file owns transport and persistence input contracts only.
507+
508+
It should contain:
509+
510+
- import and upsert DTOs
511+
- compiled row DTOs
512+
- no Prisma reads
513+
- no normalization logic
514+
- no routing logic
515+
516+
#### `wiki-actions.ts`
517+
518+
This file is the admin-facing translation boundary.
519+
520+
It should contain:
521+
522+
- conversion between admin tab rows and domain input DTOs
523+
- normalization of user-edited arrays and mapping rows
524+
- orchestration of save operations through import writers
525+
- projection from persisted state back into admin datastore rows
526+
527+
It should not contain:
528+
529+
- request-time routing logic
530+
- bag selection rules
531+
- direct write logic that bypasses import writers
532+
533+
#### `wiki-import.ts`
534+
535+
This file owns authored-state persistence and compilation.
536+
537+
It should contain:
538+
539+
- upsert behavior for roles, users, bags, templates, and wikis
540+
- rename propagation rules for authored definitions
541+
- compilation from authored JSON into derived recipe rows
542+
- validation that referenced bags and plugins exist before commit
543+
544+
It should not contain:
545+
546+
- route handling
547+
- request-time tiddler reads and writes
548+
- ad hoc resolver behavior
549+
550+
#### `RecipeResolver.ts`
551+
552+
This file is the only authority for request-time wiki resolution.
553+
554+
It should contain:
555+
556+
- wiki access gating for runtime endpoints
557+
- read resolution for one title and many titles
558+
- write target resolution for one title and many titles
559+
- list, read, save, delete, and update semantics derived from one shared routing model
560+
- index-page data assembly for an already-compiled wiki view
561+
562+
It should not contain:
563+
564+
- admin save orchestration
565+
- compilation from template/wiki definitions into derived recipe rows
566+
- direct knowledge of UI tab storage shapes
567+
568+
#### `wiki-store.ts`
569+
570+
This file owns low-level tiddler persistence and event emission.
571+
572+
It should contain:
573+
574+
- upsert and delete against the concrete bag target
575+
- append-only event creation
576+
577+
It should not contain:
578+
579+
- permission decisions
580+
- bag routing decisions
581+
- admin data transformations
582+
583+
#### `wiki-routes.ts`
584+
585+
This file is a transport shell.
586+
587+
It should contain:
588+
589+
- HTTP route declarations
590+
- request validation
591+
- auth preconditions delegated to resolver methods
592+
- transaction boundaries
593+
- translation between HTTP payloads and resolver calls
594+
595+
It should not contain:
596+
597+
- title routing rules
598+
- bag selection logic
599+
- duplicated permission policy
600+
601+
#### `wiki-utils.ts`
602+
603+
This file should stay narrowly generic.
604+
605+
It should contain only helpers that are:
606+
607+
- domain-neutral
608+
- side-effect free unless their name makes the side effect obvious
609+
- too small to deserve their own module
610+
611+
### Enforcement Rules
612+
613+
These rules are the practical way to keep the structure from drifting again.
614+
615+
- Only `RecipeResolver` may decide `readFrom(title)` and `writeTo(title)`.
616+
- Only the import layer may compile authored JSON into `RecipeBag` and related derived rows.
617+
- Route handlers may validate inputs and start transactions, but may not implement wiki behavior.
618+
- `WikiStore` receives a concrete `bag_id`; it never figures out which bag to touch.
619+
- Admin save paths must go through the import writers; they should not issue parallel ad hoc Prisma updates elsewhere.
620+
- Authored JSON should be treated as immutable input. Any normalization or merge should produce new values rather than mutating caller-owned objects in place.
621+
- Runtime failures that cross module boundaries should be structured errors, not strings.
622+
- When a rule exists in both comments and code, tests should be written against the rule and the comment should be treated as a contract, not decoration.
623+
624+
## Remaining Convergence Work
625+
626+
The remaining work is not random cleanup. It is a short list of places where the implementation still violates the structure above or leaves the behavior under-specified.
627+
628+
### 1. Unify the Permission Model in One Runtime Contract
629+
630+
This is the highest-priority inconsistency.
631+
632+
Today the code has two overlapping gates:
633+
634+
- `assertRecipe(...)`
635+
- `assertRecipeAccess(...)`
636+
637+
Those functions are close in purpose but not identical in behavior. They should either collapse into one runtime access contract or be split into clearly different responsibilities with names that reflect the split.
638+
639+
Required outcome:
640+
641+
- one definition of wiki read eligibility
642+
- one definition of wiki write eligibility
643+
- one consistent interpretation of permission hierarchy
644+
- no route that can pass one gate and fail another for the same reason
645+
646+
### 2. Remove Policy Duplication From the Route Layer
647+
648+
The route file is still doing repeated access choreography before every resolver call. That is a maintainability smell even if the logic is correct.
649+
650+
Required outcome:
651+
652+
- route handlers become thin wrappers
653+
- shared authorization and wiki-loading flow moves behind resolver-facing helpers
654+
- route code stops repeating the same state mutations and access calls
655+
656+
### 3. Normalize Batch Error Semantics
657+
658+
The design now says batch operations fail as a whole, while storage-level atomicity remains undefined. The implementation should reflect that explicitly rather than accidentally through whatever exception happens to escape.
659+
660+
Required outcome:
661+
662+
- resolver batch methods have one clear failure contract for validation, authorization, and routing errors
663+
- expected batch rejection uses structured errors, not raw thrown strings
664+
- transport code does not have to infer whether a thrown failure means full rejection or partial success
665+
- the code and docs do not imply transactionality unless the implementation actually guarantees it
666+
667+
### 4. Separate Pure Compilation From Persistence Side Effects
668+
669+
The compiler shape is good, but the layer is still too coupled to persistence concerns.
670+
671+
Required outcome:
672+
673+
- compilation logic can be reasoned about as a pure authored-definition to compiled-definition transform except for referenced-row lookup
674+
- persistence code applies the compiled result transactionally
675+
- rename propagation and recompilation rules are explicit about whether they edit authored definitions, compiled rows, or both
676+
677+
### 5. Replace Type-System Escape Hatches With Domain Types
678+
679+
Some of the remaining roughness comes from generic importer machinery and `any`-style escape hatches. Those shortcuts were useful during scaffolding, but they now obscure the domain model.
680+
681+
Required outcome:
682+
683+
- reduce mutation-through-casts patterns
684+
- reduce generic Prisma indirection where concrete model-specific logic is clearer
685+
- prefer explicit domain return types when a module is part of the core wiki model
686+
687+
This does not mean removing every abstraction. It means keeping only the abstractions that make the rules easier to audit.
688+
689+
### 6. Make Status, List, Read, and Update Contracts Explicitly Coherent
690+
691+
The system goal is that all endpoint forms expose the same wiki view. That is mostly true structurally, but the exact payload contract is still partly implied by the code.
692+
693+
Required outcome:
694+
695+
- document which routing facts appear in status
696+
- document which routing facts appear in list and read responses
697+
- document which events count as meaningful updates for a wiki view
698+
- write tests that assert those surfaces agree on the same underlying resolution rules
699+
700+
### 7. Define Rename and Recompile Guarantees
701+
702+
Bag and template edits can affect many dependent rows. The code already attempts to keep that consistent, but the guarantees are not yet stated strongly enough.
703+
704+
Required outcome:
705+
706+
- define when authored definitions are rewritten during rename
707+
- define when dependent wikis are fully recompiled versus lightly rewritten
708+
- define whether partial completion is acceptable during fanout operations
709+
710+
### 8. Add Structural Tests Before Further Feature Work
711+
712+
The next productive step is not more feature branching. It is locking the existing model with tests around the invariants already claimed in this document.
713+
714+
Minimum priority tests:
715+
716+
- permission hierarchy and gating
717+
- resolver agreement across list, read, save, delete, and updates
718+
- prefix longest-match routing
719+
- delete uncover semantics
720+
- template-save recompilation of dependent wikis
721+
- batch rejection on one denied or invalid item
722+
- batch behavior under partial-write risk until atomicity is explicitly defined
723+
724+
## Practical Plan
725+
726+
If the goal is to lock the structure down without you manually rewriting everything first, the realistic sequence is:
727+
728+
1. Treat this document as the contract for the current subsystem.
729+
2. Refactor only where the code violates one of the module boundaries or invariants above.
730+
3. Add tests for the runtime invariants before adding another wiki type or more admin behavior.
731+
4. After the tests are in place, simplify internals aggressively where they are still carrying scaffolding-era compromises.
732+
733+
That is enough to move the subsystem from "partially stabilized" to "owned by a design" without pretending the remaining cleanup is trivial.

0 commit comments

Comments
 (0)