@drhomet/mithaq 0.1.1 → 0.2.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/LICENSES/openstatus-time-picker.txt +21 -0
- package/README.md +161 -0
- package/dist/asas/build.d.ts +10 -2
- package/dist/asas/build.js +69 -22
- package/dist/asas/cli/mithaq.js +35 -3
- package/dist/asas/cli/upgrade.d.ts +22 -9
- package/dist/asas/cli/upgrade.js +187 -78
- package/dist/asas/http/fetch.d.ts +21 -3
- package/dist/asas/http/fetch.js +123 -60
- package/dist/asas/http/orpc.d.ts +5 -3
- package/dist/asas/http/orpc.js +126 -108
- package/dist/asas/runner/bulk.d.ts +1 -1
- package/dist/asas/runner/bulk.js +16 -11
- package/dist/asas/runner/errors.d.ts +5 -2
- package/dist/asas/runner/errors.js +18 -2
- package/dist/asas/runner/read.d.ts +6 -2
- package/dist/asas/runner/read.js +28 -6
- package/dist/asas/runner/steps.d.ts +22 -4
- package/dist/asas/runner/steps.js +15 -9
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +2 -2
- package/dist/asas/runner/tenant.js +78 -16
- package/dist/asas/runner/write.d.ts +173 -11
- package/dist/asas/runner/write.js +274 -46
- package/dist/auth/better-auth/access.d.ts +14 -0
- package/dist/auth/better-auth/access.js +32 -0
- package/dist/auth/better-auth/generate.d.ts +3 -1
- package/dist/auth/better-auth/generate.js +26 -8
- package/dist/auth/better-auth/index.d.ts +2 -0
- package/dist/auth/better-auth/tenant.d.ts +11 -3
- package/dist/auth/better-auth/tenant.js +10 -2
- package/dist/auth/local/index.js +5 -3
- package/dist/base/fields.d.ts +3 -2
- package/dist/base/fields.js +3 -2
- package/dist/base/index.d.ts +2 -1
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +21 -0
- package/dist/base/kinds.js +46 -1
- package/dist/base/rules.d.ts +4 -4
- package/dist/base/rules.js +4 -3
- package/dist/drizzle/index.d.ts +9 -139
- package/dist/drizzle/index.js +143 -24
- package/dist/drizzle/kinds.d.ts +1 -0
- package/dist/drizzle/kinds.js +3 -1
- package/dist/drizzle/migrate.d.ts +26 -0
- package/dist/drizzle/migrate.js +69 -26
- package/dist/drizzle/rules.d.ts +1 -1
- package/dist/index.d.ts +11 -21
- package/dist/index.js +8 -18
- package/dist/mithaq/define.d.ts +62 -15
- package/dist/mithaq/define.js +23 -9
- package/dist/mithaq/generate-messages.js +5 -4
- package/dist/mithaq/language.d.ts +43 -4
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.js +5 -0
- package/dist/mithaq/resolve.js +70 -10
- package/dist/mithaq/types.d.ts +29 -1
- package/dist/rbac/index.d.ts +2 -2
- package/dist/rbac/index.js +1 -1
- package/dist/rich-text/index.d.ts +1 -0
- package/dist/rich-text/index.js +1 -0
- package/dist/rich-text/rich-text.d.ts +2 -55
- package/dist/rich-text/rich-text.js +2 -125
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +5 -2
- package/dist/screen/client.js +5 -2
- package/dist/screen/generate.js +48 -6
- package/dist/screen/index.d.ts +18 -1
- package/dist/screen/index.js +24 -6
- package/dist/screen/react.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -3
- package/dist/storage/index.js +1 -2
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +9 -2
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +12 -3
- package/dist/zod/index.js +6 -5
- package/dist/zod/kinds.d.ts +3 -1
- package/dist/zod/kinds.js +11 -2
- package/dist/zod/rules.d.ts +1 -1
- package/docs/packages/mithaq/README.md +962 -0
- package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
- package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
- package/docs/packages/mithaq/src/drizzle/README.md +39 -0
- package/docs/packages/mithaq-admin/README.md +312 -0
- package/docs/templates/asas/README.md +1056 -0
- package/package.json +37 -8
- package/skills/add-field/SKILL.md +27 -0
- package/skills/add-kayan/SKILL.md +26 -0
- package/skills/eject-admin-piece/SKILL.md +28 -0
- package/skills/upgrade-mithaq/SKILL.md +35 -0
- package/r/mithaq-cells.json +0 -20
- package/r/mithaq-controls.json +0 -37
- package/r/mithaq-directional-icon.json +0 -15
- package/r/mithaq-expected-updated-at.json +0 -15
- package/r/mithaq-field-shell.json +0 -18
- package/r/mithaq-filter-operators.json +0 -15
- package/r/mithaq-form.json +0 -33
- package/r/mithaq-list-columns.json +0 -23
- package/r/mithaq-list-filters.json +0 -24
- package/r/mithaq-list-pagination.json +0 -24
- package/r/mithaq-list-query.json +0 -18
- package/r/mithaq-list-selection.json +0 -19
- package/r/mithaq-list.json +0 -33
- package/r/mithaq-nav.json +0 -24
- package/r/mithaq-nodes.json +0 -34
- package/r/mithaq-relationship-cell.json +0 -20
- package/r/mithaq-relationship-control.json +0 -20
- package/r/mithaq-rich-text.json +0 -40
- package/r/mithaq-screens-host.json +0 -96
- package/r/mithaq-storage.json +0 -44
- package/r/mithaq-upload.json +0 -29
- package/r/registry.json +0 -550
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# The admin package
|
|
2
|
+
|
|
3
|
+
The controls, cells, forms, lists and shells a screen draws with ship as the
|
|
4
|
+
package `@drhomet/mithaq-admin`, which a project installs under the name its
|
|
5
|
+
imports use, `"mithaq-admin": "npm:@drhomet/mithaq-admin@<version>"`, at the
|
|
6
|
+
version of `mithaq`. Each piece is a TSX file the project imports as
|
|
7
|
+
`mithaq-admin/<file>`, and compiles against its own shadcn primitives through
|
|
8
|
+
`@/components/ui/*` and `@/lib/utils`, so the admin takes the app's style.
|
|
9
|
+
|
|
10
|
+
How a project adds its own components is in [Add a screen component](https://github.com/drhomet/mithaq/blob/main/templates/asas/README.md#add-a-screen-component).
|
|
11
|
+
|
|
12
|
+
## Set up an app
|
|
13
|
+
|
|
14
|
+
Each app wires the package in three places, all visible to the project:
|
|
15
|
+
|
|
16
|
+
- `vite.config.ts`: the `mithaq/vite` plugin, named with the folder pieces are ejected into.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { mithaqAdmin } from 'mithaq/vite'
|
|
20
|
+
// ...
|
|
21
|
+
plugins: [tailwindcss(), mithaqAdmin(fileURLToPath(new URL('./src/components/mithaq/', import.meta.url))), /* ... */]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It keeps the package out of Vite's pre-bundling, so a piece the app imports and the same piece another piece
|
|
25
|
+
imports are one module, and it serves an ejected file in place of the package's for every importer. It pre-bundles
|
|
26
|
+
from the start the libraries the pieces import that the project installed, so a cold start never finds one
|
|
27
|
+
mid-page and reloads it.
|
|
28
|
+
- `tsconfig.json` `paths`: `"mithaq-admin/*": ["./src/components/mithaq/*", "./node_modules/mithaq-admin/registry/*"]`.
|
|
29
|
+
TypeScript tries the ejected folder first, so an ejected file is checked in place of the package's, then the
|
|
30
|
+
package, which a piece installed in a store's own folder cannot reach by its name.
|
|
31
|
+
- `styles.css`: `@import 'mithaq-admin/source.css';`, because Tailwind never scans `node_modules`.
|
|
32
|
+
|
|
33
|
+
Every library a piece imports is a peer dependency of the package: `mithaq`, React, TanStack Form, the two icon
|
|
34
|
+
sets, `input-otp` and `react-day-picker`, and, for the rich text add-on, Lexical.
|
|
35
|
+
|
|
36
|
+
A project holds a few files of the package from creation, because they import the app's own files or hold
|
|
37
|
+
policy: the screen host (`mithaq-screens-host`), the storage adapters and defaults (`mithaq-storage`), the mailers (`mithaq-mail`), the
|
|
38
|
+
`expectedUpdatedAt` step (`mithaq-expected-updated-at`) and `ui/directional-icon.tsx`. They are the project's
|
|
39
|
+
to edit.
|
|
40
|
+
|
|
41
|
+
## Eject a piece
|
|
42
|
+
|
|
43
|
+
`mithaq eject <item>`, run in the app, writes the item's own files into `src/components/mithaq/` byte for byte,
|
|
44
|
+
each ending with a marker line, `// mithaq-admin <version> <hash>`. From then on that file is drawn in place of
|
|
45
|
+
the package's by every importer, the package's own pieces among them. Edit it in place. Delete it to draw the
|
|
46
|
+
package's again, or run `mithaq eject --prune`, which deletes every ejected file nobody edited. Restart the dev
|
|
47
|
+
server after an eject or a prune.
|
|
48
|
+
|
|
49
|
+
Eject copies no other piece, because the package already serves each one, never overwrites a file, and installs
|
|
50
|
+
nothing: it names the shadcn primitives and npm packages the item needs that the project lacks. [The `mithaq`
|
|
51
|
+
command](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/cli/README.md#eject-and-eject---prune) has the details.
|
|
52
|
+
|
|
53
|
+
Reach for the components map first: a control, a cell or a node is replaced there without ejecting anything. Eject
|
|
54
|
+
when the piece you want to change is not in the map, such as `list-table.tsx` inside the list.
|
|
55
|
+
|
|
56
|
+
## `mithaq upgrade --check`
|
|
57
|
+
|
|
58
|
+
`mithaq upgrade --check` compares every file the project holds (what it ejected and the files it always holds)
|
|
59
|
+
with the installed package, one line per file, and never writes; you merge a change by hand. Where it lives:
|
|
60
|
+
`packages/mithaq/src/asas/cli/upgrade.ts` `upgradeCheck`.
|
|
61
|
+
[The `mithaq` command](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/cli/README.md#upgrade---check) describes it.
|
|
62
|
+
|
|
63
|
+
When you eject `mithaq-upload` from a version before rich text and files became
|
|
64
|
+
add-ons, `upload.tsx` no longer exports `UploadPlugin`, `UploadView`, `imagePick`
|
|
65
|
+
or `INSERT_UPLOAD_COMMAND`: import them from `mithaq-admin/upload-node.tsx` in your
|
|
66
|
+
own `ui/rich-text-nodes.tsx`, and spread `richTextComponents` and
|
|
67
|
+
`uploadComponents` into your components map (see
|
|
68
|
+
[Rich text and files](#rich-text-and-files)).
|
|
69
|
+
|
|
70
|
+
## Migrate a project that holds copies
|
|
71
|
+
|
|
72
|
+
A project made before the package holds every piece as a copy under `src/components/mithaq/`. In this order:
|
|
73
|
+
|
|
74
|
+
1. Install `mithaq-admin` at the version of `mithaq`, then add the plugin, the `paths` entry and the
|
|
75
|
+
`source.css` import shown in [Set up an app](#set-up-an-app).
|
|
76
|
+
2. Run `bunx mithaq eject --prune` in each app. Every copy you never edited goes, stale ones included, and the
|
|
77
|
+
package draws the current piece in its place.
|
|
78
|
+
3. In what remains under `src/components/mithaq/`, and in every other file, rewrite the imports: `'./<file>'`
|
|
79
|
+
inside that folder, `'./components/mithaq/<file>'` and `'@/components/mithaq/<file>'` all become
|
|
80
|
+
`'mithaq-admin/<file>'`. Step 2 comes first, because a rewritten copy no longer hashes to its marker and
|
|
81
|
+
would look edited.
|
|
82
|
+
4. Run `bunx mithaq upgrade --check`. Each edited copy reports `differs, edited`; merge each by hand once.
|
|
83
|
+
5. Move the components map out of `src/router.tsx` into `src/components.ts`, and import it from `router.tsx` and
|
|
84
|
+
`server.ts`, so the server entry reads the map without evaluating the router. In a monorepo, add
|
|
85
|
+
`apps/*/src/components.ts` to the root tsconfig's `exclude` beside `apps/*/src/server.ts`: the map imports
|
|
86
|
+
the package's `.tsx`, which the app's own config checks.
|
|
87
|
+
6. Delete the `@mithaq` entry from each `components.json` `registries`; `shadcn add` is for primitives now.
|
|
88
|
+
|
|
89
|
+
## Add or replace a control
|
|
90
|
+
|
|
91
|
+
Every screen draws a field's control, and a list or a read-only value its
|
|
92
|
+
cell, from the components map the app hands `ComponentsProvider`. The app keeps the map in `src/components.ts`,
|
|
93
|
+
typed against the project's language, and mounts it around every route in `src/router.tsx`:
|
|
94
|
+
|
|
95
|
+
<!-- from: apps/asas-tanstack/src/components.ts -->
|
|
96
|
+
```ts
|
|
97
|
+
export const components: ComponentsOf<Language> = {
|
|
98
|
+
...defaultComponents,
|
|
99
|
+
controls: {
|
|
100
|
+
...defaultComponents.controls,
|
|
101
|
+
// ...
|
|
102
|
+
},
|
|
103
|
+
cells: {
|
|
104
|
+
...defaultComponents.cells,
|
|
105
|
+
// ...
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
<!-- from: apps/asas-tanstack/src/router.tsx -->
|
|
111
|
+
```tsx
|
|
112
|
+
Wrap: ({ children }) => <ComponentsProvider components={components}>{children}</ComponentsProvider>,
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To draw a shipped kind or control name with your own control, spread the
|
|
116
|
+
defaults and replace that one name; a cell is replaced the same way under
|
|
117
|
+
`cells`. A kind your own rukn brings, which no control or cell the registry
|
|
118
|
+
ships draws, names itself as its control or its cell, and the map draws it
|
|
119
|
+
under that name:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const color = kind<string>(isColor)({
|
|
123
|
+
label: { en: 'Color', ar: 'لون' },
|
|
124
|
+
message: { en: 'This is not a color.', ar: 'هذا ليس لونا.' },
|
|
125
|
+
rules: ['required'],
|
|
126
|
+
targets: { drizzle: drizzleKinds.text, zod: zodKinds.text, screen: { control: 'color', cell: 'color' } },
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const components: ComponentsOf<Language> = {
|
|
132
|
+
...defaultComponents,
|
|
133
|
+
controls: { ...defaultComponents.controls, color: ColorControl, text: MyTextControl },
|
|
134
|
+
cells: { ...defaultComponents.cells, color: ColorCell },
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`ComponentsOf<Language>` requires one control and one cell per name
|
|
139
|
+
`defaultComponents` holds, and one of each per kind of the language that no shipped one
|
|
140
|
+
draws, so leaving `color` out, naming a key that is neither, or a control that
|
|
141
|
+
does not take `ControlProps` or a cell that does not take `CellProps` is a type
|
|
142
|
+
error. A kind the language declares needs its entry
|
|
143
|
+
even when no kayan uses it, since the type reads the language and not the
|
|
144
|
+
kayan. A kind drawn by a `ui()` reference needs a map entry too, because an
|
|
145
|
+
operation's dialog, the upload dialog and a form drawn with no page draw it
|
|
146
|
+
through the map. For the same reason the type does not see a cell that a list
|
|
147
|
+
column or a leaf names on a kayan's page: a column naming its kind's own name
|
|
148
|
+
(`{ field: 'color', cell: 'color' }` where the kind draws `text`) needs
|
|
149
|
+
`cells.color` in the map without the type asking for it.
|
|
150
|
+
|
|
151
|
+
Each app's `src/server.ts` checks every screen it serves against the map as it
|
|
152
|
+
starts, through `checkComponents`, so a field whose kind has no control or no
|
|
153
|
+
cell stops the start with the kind and the field named, and a cell a list
|
|
154
|
+
column or a leaf names that the map does not carry stops it with the screen,
|
|
155
|
+
the page, the node and the cell named. On the page, such a cell is an error and
|
|
156
|
+
never the field's own cell in its place. A route that throws while drawing
|
|
157
|
+
is replaced by `RouteError`, the router's `defaultErrorComponent`, and the shell
|
|
158
|
+
around it stays.
|
|
159
|
+
|
|
160
|
+
## Rich text and files
|
|
161
|
+
|
|
162
|
+
The controls and cells of the `richText` kind, which the rich text rukn
|
|
163
|
+
brings, and of the `upload` kind, which the storage rukn brings, are not in
|
|
164
|
+
`defaultComponents`. Each ships as an add-on map of its own,
|
|
165
|
+
`richTextComponents` in `rich-text.tsx` and `uploadComponents` in `upload.tsx`,
|
|
166
|
+
and a project spreads one only where its language holds the kind. A project
|
|
167
|
+
without rich text never loads Lexical's editor, and one without files never
|
|
168
|
+
loads the upload code:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
const components: ComponentsOf<Language> = {
|
|
172
|
+
...defaultComponents,
|
|
173
|
+
controls: { ...defaultComponents.controls, ...richTextComponents.controls, ...uploadComponents.controls },
|
|
174
|
+
cells: { ...defaultComponents.cells, ...richTextComponents.cells, ...uploadComponents.cells },
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`ComponentsOf<Language>` asks for an add-on's control and cell whenever the
|
|
179
|
+
language declares the kind and a screen's controls table draws it with the
|
|
180
|
+
add-on's name, even when no kayan on any screen uses it, since the type reads
|
|
181
|
+
the language and not the kayan. Leaving one out is a type error, and
|
|
182
|
+
`checkComponents` stops the start naming the map to spread. A replaced
|
|
183
|
+
`cells.richText` or `cells.upload` is drawn on record pages too, handed
|
|
184
|
+
`whole`, and must draw the full value when `whole` is set. The rich
|
|
185
|
+
text upload node (`UploadView`, `imagePick` and `UploadPlugin`) is the item
|
|
186
|
+
`mithaq-upload-node`, for a project that has both.
|
|
187
|
+
|
|
188
|
+
## Replace a node
|
|
189
|
+
|
|
190
|
+
A page's tree is drawn from the same map, one component per kind of node under
|
|
191
|
+
`nodes`. To draw tabs, a section or any other shipped node your own way, spread
|
|
192
|
+
`defaultComponents.nodes` and replace that kind; the component is handed `NodeProps` of
|
|
193
|
+
its kind, and draws the nodes it holds with `drawNodes`:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
const MySection = ({ node, draw }: NodeProps<'section'>) => <fieldset>{drawNodes({ nodes: node.fields, draw })}</fieldset>
|
|
197
|
+
|
|
198
|
+
const components: ComponentsOf<Language> = {
|
|
199
|
+
...defaultComponents,
|
|
200
|
+
nodes: { ...defaultComponents.nodes, section: MySection },
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The map takes one entry per kind `mithaq/admin` declares and no other: a
|
|
205
|
+
screen's build refuses a node of any other kind, so no page could hold one.
|
|
206
|
+
Draw your own thing with a `component` node instead. `checkComponents` stops
|
|
207
|
+
the start when a page holds a kind the map does not carry, naming the screen,
|
|
208
|
+
the kayan and the page, and drawing one is an error on the page.
|
|
209
|
+
|
|
210
|
+
## Moving users off Payload
|
|
211
|
+
|
|
212
|
+
A project that moves its users off Payload keeps their passwords with the
|
|
213
|
+
`mithaq-payload-password` item, and deletes it once they have moved. Payload
|
|
214
|
+
hashes with pbkdf2-sha256, and Better Auth with scrypt; the item lets Better
|
|
215
|
+
Auth verify both, and replaces each Payload hash with Better Auth's the first
|
|
216
|
+
time its user signs in.
|
|
217
|
+
|
|
218
|
+
1. Eject it into the language, where the Better Auth setup lives. In
|
|
219
|
+
`packages/mithaq` (the project's root in a single package):
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
bunx mithaq eject payload-password # writes src/mithaq/auth/payload-password.ts
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
2. Wire it into `optionsFor` in `src/mithaq/auth/index.ts`, which both apps'
|
|
226
|
+
instances build on:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { payloadPassword, payloadRehash } from './payload-password.ts'
|
|
230
|
+
|
|
231
|
+
emailAndPassword: { enabled: true, password: payloadPassword() /* , ... */ },
|
|
232
|
+
plugins: [payloadRehash() /* , ... */],
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- `payloadPassword()` hashes every new password with Better Auth's own
|
|
236
|
+
scrypt. Its `verify` checks a `payload-pbkdf2$<salt>$<hash>` value as
|
|
237
|
+
Payload 3.89 and older wrote it (25000 iterations, a 512-byte key) and a
|
|
238
|
+
`payload-pbkdf2-v1$<salt>$<hash>` one as Payload 3.90 does (600000
|
|
239
|
+
iterations, a 32-byte key), and hands anything else to Better Auth's.
|
|
240
|
+
A malformed value refuses the password; it never throws.
|
|
241
|
+
- `payloadRehash()` runs after `/sign-in/email` and `/sign-in/username`.
|
|
242
|
+
When the sign-in succeeded on a Payload hash, it verifies the password
|
|
243
|
+
against the hash it reads again, then writes Better Auth's hash of it only
|
|
244
|
+
over that hash, so neither a cutover run meanwhile nor a password set
|
|
245
|
+
meanwhile is overwritten. A failed write is logged and never fails the
|
|
246
|
+
sign-in; the next sign-in tries again. The prefix is the only flag: no
|
|
247
|
+
column says who has moved. A project that signs in with the phone-number
|
|
248
|
+
plugin adds `/sign-in/phone-number` to its matcher, `signIns`.
|
|
249
|
+
- No password length is capped on sign-in. Payload let a password of up to
|
|
250
|
+
40000 characters be set, past Better Auth's `maxPasswordLength` of 128,
|
|
251
|
+
which caps only a password being set.
|
|
252
|
+
- A known limit while users move: sign-in timing tells the address of a user
|
|
253
|
+
still on a Payload 3.90 hash from an unknown one, about 105 to 130 ms of
|
|
254
|
+
pbkdf2 against about 60 ms of scrypt. It ends as each user is rehashed.
|
|
255
|
+
|
|
256
|
+
3. At cutover, give each account its user's Payload hash. Payload stores `salt`
|
|
257
|
+
and `hash` in two columns of its `users` table, and
|
|
258
|
+
`payloadHashToAccountPassword({ salt, hash })` makes the one string
|
|
259
|
+
`account.password` holds, throwing on a pair Payload never wrote. Where users
|
|
260
|
+
were mirrored into Better Auth under a shared temporary password, only the
|
|
261
|
+
accounts still on it take their Payload hash, which retires the temporary
|
|
262
|
+
password; a user who chose a password in Better Auth keeps it. A script run
|
|
263
|
+
once, with Payload's `users` table in the same database:
|
|
264
|
+
|
|
265
|
+
<!-- from: packages/mithaq-admin/test/payload-password.test.ts -->
|
|
266
|
+
```ts
|
|
267
|
+
const cutover = async ({ db, temporary }: { db: pg.Pool; temporary: string }) => {
|
|
268
|
+
const { verify } = payloadPassword()
|
|
269
|
+
const { rows } = await db.query<{ id: string; password: string; salt: string; hash: string }>(`
|
|
270
|
+
select account.id, account.password, users.salt, users.hash
|
|
271
|
+
from account
|
|
272
|
+
join "user" on "user".id = account.user_id
|
|
273
|
+
join users on lower(users.email) = lower("user".email)
|
|
274
|
+
where account.provider_id = 'credential' and account.password is not null and users.hash is not null`)
|
|
275
|
+
let moved = 0
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
if (!(await verify({ hash: row.password, password: temporary }))) continue
|
|
278
|
+
const { rowCount } = await db.query('update account set password = $1 where id = $2 and password = $3', [payloadHashToAccountPassword(row), row.id, row.password])
|
|
279
|
+
moved += rowCount ?? 0
|
|
280
|
+
}
|
|
281
|
+
return moved
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Read the temporary password from the environment and print the count, never
|
|
286
|
+
a password or a hash. A rerun moves nobody, so the script runs in the
|
|
287
|
+
rehearsal and again on the day. It stops at the first pair Payload never
|
|
288
|
+
wrote; the accounts before it have moved, and a rerun after the fix skips
|
|
289
|
+
them. A Payload user with no Better Auth account needs one made first, with
|
|
290
|
+
its credential account's `password` set the same way.
|
|
291
|
+
|
|
292
|
+
4. Delete the file once every imported user has signed in, which is when
|
|
293
|
+
`select count(*) from account where password like 'payload-pbkdf2%'` answers
|
|
294
|
+
0, or once the window you give them closes. Clear what is left first:
|
|
295
|
+
Better Auth's own `verify` throws on a Payload hash, so a sign-in on one
|
|
296
|
+
would fail with a server error. `update account set password = null where
|
|
297
|
+
password like 'payload-pbkdf2%'` leaves those users the password reset.
|
|
298
|
+
Then remove `password` and `payloadRehash()` from `optionsFor`.
|
|
299
|
+
|
|
300
|
+
## Add a registry item
|
|
301
|
+
|
|
302
|
+
A registry item is a piece mithaq ships for every project. Its source lives in `packages/mithaq-admin/registry/`:
|
|
303
|
+
|
|
304
|
+
1. Write the file under `packages/mithaq-admin/registry/`, and list it as an item in `packages/mithaq-admin/registry.json` with its `target`, the bare names of the shadcn primitives it imports in `registryDependencies`, and the npm packages it alone needs in `dependencies`.
|
|
305
|
+
2. Import a sibling piece as `mithaq-admin/<file>`, never by a relative path, so a project's ejected copy of the sibling is drawn in its place.
|
|
306
|
+
3. Add the item's name to the list in `packages/mithaq-admin/test/render.test.ts` `the app holds the current source of every registry item it copied`.
|
|
307
|
+
4. A library the piece imports that no other piece does is a peer dependency in `packages/mithaq-admin/package.json`.
|
|
308
|
+
5. Run `bun run test`. A piece the apps import is drawn from the package at once; only a file a project always holds (the host, storage, a step) is ejected again in `templates/mithaq` or the apps: delete the old file and run `bunx mithaq eject <item>` there.
|
|
309
|
+
|
|
310
|
+
### A registry step
|
|
311
|
+
|
|
312
|
+
A step every project may want, and may want to change, ships as a registry file rather than as core code. `expectedUpdatedAt` is one: it lives in `packages/mithaq-admin/registry/steps/expected-updated-at.ts` in the mithaq repository, `mithaq eject mithaq-expected-updated-at` writes it to `src/steps/expected-updated-at.ts`, and a project edits its copy in place. Write a new one the same way, and follow [Add a registry item](#add-a-registry-item). Put the step's tests beside it so a replacement can run them.
|