@drhomet/mithaq 0.1.1 → 0.2.1
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 +974 -0
- package/docs/packages/mithaq/src/asas/cli/README.md +283 -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 +1094 -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,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OpenStatus
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# mithaq
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@drhomet/mithaq)
|
|
4
|
+
|
|
5
|
+
mithaq is a TypeScript language for declaring a backend and its admin once, and building the database, API, validation, access rules and admin screens from that one declaration.
|
|
6
|
+
|
|
7
|
+
The package is `@drhomet/mithaq`. It is pre-1.0, so the API may still change between releases.
|
|
8
|
+
|
|
9
|
+
The name is Arabic for a covenant. The other words in the API are Arabic too, and each is defined where it first appears below.
|
|
10
|
+
|
|
11
|
+
## Why it exists
|
|
12
|
+
|
|
13
|
+
- **One declaration drives everything.** A kayan's fields, kinds and rules become the Postgres table (through drizzle), the input schema (through zod), the HTTP operations and the admin forms and lists. Nothing is declared twice, so nothing drifts.
|
|
14
|
+
- **The vocabulary is frozen.** The kinds, rules and targets mithaq ships change only by the maintainer's explicit decision. A project can also seal its own language in a signed `mithaq.lock`. `mithaq lock --check`, which the apps run before `dev`, `build` and `preview`, then fails when the vocabulary no longer matches the lock. Signing a change takes a code shown in a popup, never in the command's output, so a script or an agent running `mithaq lock` cannot approve it.
|
|
15
|
+
- **Tenant isolation is enforced in core.** A kayan that places the tenant field belongs to one organization. Core adds the tenant scope to every operation and combines it with the project's own access with AND, so no access rule can widen it. Writes land in the active organization, and `unique` rules and index groups are built per organization.
|
|
16
|
+
- **Defaults are explicit.** The project names every default it uses (`defaultOperations`, `defaultFields`, `defaultAuthorize`, the limits) in its own code. Core does not fall back to a default of its own: a configuration that leaves a required one out is a type error or a refusal from `buildAsas`.
|
|
17
|
+
- **The admin is your code when you want it.** The screens are shadcn components the project imports from `mithaq-admin` and draws with its own primitives; `mithaq eject` puts any one of them into the project to edit like any other file.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bun create mithaq my-app --full
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This creates a Bun workspace of the template's three steps, the language (`packages/mithaq`), a package per feature (`packages/features/*`) and the asas that builds them (`packages/asas`), and two apps: the tenant app (`apps/asas-tanstack`), which serves the dashboard and the portal, and the backoffice console (`apps/asas-backoffice`), for staff who work across every organization. It writes a `.env` with fresh secrets and runs `bun install`. `--full` takes every add-on, the whole worked example; without it the project is minimal, auth and a `tags` example, and `--with roles,courses` picks add-ons.
|
|
26
|
+
|
|
27
|
+
Then, with PostgreSQL 15 or later serving the database that `DATABASE_URL` names in `packages/asas/.env`:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
cd my-app
|
|
31
|
+
cd packages/asas
|
|
32
|
+
# push the schema, make the dev admin and fill its organization, development only
|
|
33
|
+
bun run dev:setup --seed
|
|
34
|
+
cd ../..
|
|
35
|
+
bun run --cwd apps/asas-tanstack dev # the tenant app, http://localhost:5173
|
|
36
|
+
bun run --cwd apps/asas-backoffice dev # in a second terminal: the backoffice console, http://localhost:5174
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sign in to both apps with `DEV_ADMIN_EMAIL` and `DEV_ADMIN_PASSWORD` from `packages/asas/.env`: the dev admin owns "Dev admin's workspace", which `--seed` filled, and is the backoffice's `super_admin`.
|
|
40
|
+
|
|
41
|
+
In production, `connect()` does not push the schema; it checks the migration files instead. [Running in production](https://github.com/drhomet/mithaq/blob/main/templates/asas/README.md#running-in-production) explains how to apply them.
|
|
42
|
+
|
|
43
|
+
## A small example
|
|
44
|
+
|
|
45
|
+
A language comes from `defineMithaq`. Its arkan (singular rukn, "pillar") are the pluggable parts that bring kinds and rules; `base` brings the shipped ones. Its targets are what each declaration is built into.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// src/mithaq.ts
|
|
49
|
+
import {
|
|
50
|
+
base, defaultAuthorize, defaultBulkLimit, defaultDepth, defaultJoinLimit, defaultLimit, defaultMaxLimit,
|
|
51
|
+
defineMithaq, drizzleAdapter, drizzleKinds, drizzleRules, drizzleIdKinds, serviceTarget, zodKinds, zodRules, zodIdKinds, zodTarget,
|
|
52
|
+
} from 'mithaq'
|
|
53
|
+
|
|
54
|
+
export const { defineKayan, buildAsas } = defineMithaq({
|
|
55
|
+
locales: ['en', 'ar'] as const,
|
|
56
|
+
arkan: [base],
|
|
57
|
+
targets: {
|
|
58
|
+
drizzle: drizzleAdapter({ connectionString: process.env.DATABASE_URL!, kinds: drizzleKinds, rules: drizzleRules, idKinds: drizzleIdKinds }),
|
|
59
|
+
zod: zodTarget({ kinds: zodKinds, rules: zodRules, idKinds: zodIdKinds }),
|
|
60
|
+
service: serviceTarget({
|
|
61
|
+
authorize: defaultAuthorize,
|
|
62
|
+
depth: defaultDepth,
|
|
63
|
+
defaultLimit,
|
|
64
|
+
maxLimit: defaultMaxLimit,
|
|
65
|
+
joinLimit: defaultJoinLimit,
|
|
66
|
+
bulkLimit: defaultBulkLimit,
|
|
67
|
+
store: 'drizzle',
|
|
68
|
+
schema: 'zod',
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A kayan ("entity") is one data type: its fields, their kinds and rules, its access and its operations. Every label carries one string per locale the language declares, and a missing one is a type error.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// src/tags.ts
|
|
78
|
+
import { defaultFields, defaultOperations } from 'mithaq'
|
|
79
|
+
import { defineKayan } from './mithaq.ts'
|
|
80
|
+
|
|
81
|
+
export const tags = defineKayan({
|
|
82
|
+
name: 'tags',
|
|
83
|
+
access: { read: () => true },
|
|
84
|
+
targets: { service: { operations: defaultOperations } },
|
|
85
|
+
fields: {
|
|
86
|
+
...defaultFields,
|
|
87
|
+
name: {
|
|
88
|
+
kind: 'text',
|
|
89
|
+
label: { en: 'Name', ar: 'الاسم' },
|
|
90
|
+
rules: { required: true, unique: true, trim: true, maxLength: 60 },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`buildAsas` builds the asas ("foundation"), the runtime:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/asas.ts
|
|
100
|
+
import { defaultMaxBodyBytes, fetchHttp } from 'mithaq'
|
|
101
|
+
import { buildAsas } from './mithaq.ts'
|
|
102
|
+
import { tags } from './tags.ts'
|
|
103
|
+
|
|
104
|
+
export const asas = buildAsas({ kayan: [tags], http: [fetchHttp({ maxBodyBytes: defaultMaxBodyBytes, onError: console.error })] })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`asas.handler(request)` answers a `Request` with a `Response`; mount it under `/api` in any server. With `fetchHttp`, `POST /api/tags/create` with a JSON body creates a tag, and `POST /api/tags/find` lists them. In development, `asas.connect()` pushes the schema to Postgres. A kayan that declares no access answers only the admin the build names, so this one opens reads to everyone.
|
|
108
|
+
|
|
109
|
+
To make a kayan belong to one organization, add the tenant target to the language and place its field:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// in defineMithaq's targets:
|
|
113
|
+
tenant: betterAuthTenant({ relationTo: 'organization', onDelete: 'restrict', newRows: 'active', organizations: 'many' }),
|
|
114
|
+
|
|
115
|
+
// in a kayan's fields, with `tenant` taken from the language's `targets`:
|
|
116
|
+
organization: tenant.field,
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`unique: true` on that kayan is then unique within each organization, and `indexes: [{ within: 'organization', unique: [['code', 'term']] }]` declares a compound one. [Tenancy](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/runner/README.md) has the full rules.
|
|
120
|
+
|
|
121
|
+
## What's in the box
|
|
122
|
+
|
|
123
|
+
- **Store:** Postgres through drizzle (`drizzleAdapter`), with a schema push in development and migration files written by `mithaq migrate` for production.
|
|
124
|
+
- **Validation:** zod schemas derived from every kayan (`zodTarget`). A field can carry its own zod shape instead.
|
|
125
|
+
- **Runtime:** the operation runner (`serviceTarget`) with the default operations (create, update, delete, find, findByID, count, updateMany, deleteMany), steps around every write, an audit step, soft delete and bulk limits.
|
|
126
|
+
- **HTTP:** plain fetch routes (`fetchHttp`), or a typed client and OpenAPI through oRPC (`mithaq/orpc`).
|
|
127
|
+
- **Access:** role-based access with permission keys per operation and per field (`rbacTarget`, `betterAuthTarget` to keep roles in Better Auth's own tables, or `memberRoleTarget` for Better Auth's own member roles alone).
|
|
128
|
+
- **Tenancy:** `betterAuthTenant`, over Better Auth organizations.
|
|
129
|
+
- **Auth:** Better Auth (`betterAuth`) or local accounts (`local`), with `mithaq generate auth` writing the Better Auth kayan.
|
|
130
|
+
- **Versions and drafts:** the `versions` rukn keeps a row's past versions, with drafts, autosave and restore.
|
|
131
|
+
- **Files:** the `storage` rukn and the `upload` kind. The template ships adapters for local disk, Cloudflare R2, S3-compatible services and UploadThing as project files.
|
|
132
|
+
- **Rich text:** the `richText` rukn, over Lexical.
|
|
133
|
+
- **Admin screens:** `screenTarget` describes each screen; `mithaq generate screens` writes its routes into the host app, and the React components come from the `mithaq-admin` package, drawn with the app's own shadcn primitives, and a project ejects the ones it wants to own. `mithaq upgrade --check` compares the files it holds with the installed version.
|
|
134
|
+
- **Copy:** `mithaq generate messages` writes the project's labels and messages to one file per kayan, and refuses a label missing a locale.
|
|
135
|
+
|
|
136
|
+
`mithaq --help` lists every command.
|
|
137
|
+
|
|
138
|
+
## Requirements
|
|
139
|
+
|
|
140
|
+
- npm, pnpm 10.6 or later, Yarn 1 or [Bun](https://bun.sh) 1.3, whichever runs `create mithaq`. Yarn 2 and later install a copy of mithaq for each workspace of a monorepo, which the build refuses, so a monorepo does not take them yet.
|
|
141
|
+
- Node.js 24 or later.
|
|
142
|
+
- PostgreSQL 15 or later.
|
|
143
|
+
- TypeScript, installed by the project.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
Each guide is the `README.md` of the folder it describes.
|
|
148
|
+
|
|
149
|
+
- [Working in a mithaq project](https://github.com/drhomet/mithaq/blob/main/templates/asas/README.md): getting started, where each file lives, adding a kayan, a feature or a step, and running in production. It ships in every created project.
|
|
150
|
+
- [The mithaq language](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/README.md): the language, kinds, rules, kayan, targets, the runner and access, and adding a rukn, a kind, a target or a configurable feature.
|
|
151
|
+
- [Tenancy](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/runner/README.md): per-organization kayan, what core enforces, and per-organization indexes.
|
|
152
|
+
- [The `mithaq` command](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/cli/README.md): every command, flag and exit code.
|
|
153
|
+
- [Migrations in production](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/drizzle/README.md): `pending: 'refuse'` or `'run'`, the advisory lock, and applying migrations by hand.
|
|
154
|
+
- [The admin package](https://github.com/drhomet/mithaq/blob/main/packages/mithaq-admin/README.md): the screen components, ejecting one, `mithaq upgrade --check`, migrating a project that holds copies, and adding a registry item.
|
|
155
|
+
- [Contributing](https://github.com/drhomet/mithaq/blob/main/CONTRIBUTING.md): setting up, the checks, the repository layout, and sending a pull request.
|
|
156
|
+
|
|
157
|
+
## Status and license
|
|
158
|
+
|
|
159
|
+
mithaq is pre-1.0. The template that `bun create mithaq` creates is the worked example, and every file there is commented.
|
|
160
|
+
|
|
161
|
+
MIT licensed; see [LICENSE](https://github.com/drhomet/mithaq/blob/main/LICENSE). mithaq includes code adapted from Payload and from openstatus's time picker, both MIT licensed; their notices are in [LICENSES/payload.txt](https://github.com/drhomet/mithaq/blob/main/LICENSES/payload.txt) and [LICENSES/openstatus-time-picker.txt](https://github.com/drhomet/mithaq/blob/main/LICENSES/openstatus-time-picker.txt).
|
package/dist/asas/build.d.ts
CHANGED
|
@@ -111,6 +111,12 @@ type AdminAccessShapeMismatch<Auth> = Auth extends {
|
|
|
111
111
|
export type Vocabulary = {
|
|
112
112
|
catalog: readonly string[];
|
|
113
113
|
statements: Readonly<Record<string, readonly string[]>>;
|
|
114
|
+
/**
|
|
115
|
+
* How many rows, across every organization, still name the auth kayan's row `id` through a
|
|
116
|
+
* relationship that restricts its delete. A provider route that deletes a person outside a
|
|
117
|
+
* transaction asks it first, since the foreign key would fail only after other rows went.
|
|
118
|
+
*/
|
|
119
|
+
referenced: (id: string) => Promise<number>;
|
|
114
120
|
};
|
|
115
121
|
/** The build's auth: the admin kayan whose actor passes default access, and where failures are reported. */
|
|
116
122
|
export type AuthConfig = {
|
|
@@ -121,6 +127,8 @@ export type AuthConfig = {
|
|
|
121
127
|
};
|
|
122
128
|
/** Handed a strategy that threw and an access target that could not answer, each as an `Error` with the cause. Required where any kayan carries auth. */
|
|
123
129
|
onError: (error: unknown) => void;
|
|
130
|
+
/** The largest body an auth route reads, answered 413 past it: `defaultMaxBodyBytes` or the project's own. Required where any kayan carries auth. */
|
|
131
|
+
maxBodyBytes: number;
|
|
124
132
|
};
|
|
125
133
|
type PublicLocalUser<K extends Kayan, L extends Language, KayanList extends readonly Kayan[]> = Omit<Row<K['fields'], L, KayanList>, ProtectedLocalField>;
|
|
126
134
|
/** One strategy per kayan carrying `auth`, typed by that provider's or that strategy's own actor. */
|
|
@@ -186,7 +194,7 @@ export declare const buildAsas: <const L extends Language, const KayanList exten
|
|
|
186
194
|
auth?: Auth;
|
|
187
195
|
/**
|
|
188
196
|
* The HTTP plugs, asked in order, so the order decides who owns a prefix two claim. Required:
|
|
189
|
-
* `fetchHttp
|
|
197
|
+
* `fetchHttp` takes no dependency; `orpcHttp` first gives `asas.router` its typed client.
|
|
190
198
|
*/
|
|
191
199
|
http: readonly [(operations: OperationTable<Procedures<KayanList, GlobalList, Declared<KayanList, GlobalList>, L>>) => Http, ...((operations: OperationTable<Procedures<KayanList, GlobalList, Declared<KayanList, GlobalList>, L>>) => HttpPlug)[]];
|
|
192
200
|
/** The project's own copy for the labels mithaq ships, as one resolver whose return shape mithaq declares. */
|
|
@@ -202,7 +210,7 @@ export declare const buildAsas: <const L extends Language, const KayanList exten
|
|
|
202
210
|
statements: Readonly<Record<string, readonly string[]>>;
|
|
203
211
|
/** Every built operation, flat. Its `procedures` view is a type, keyed by kayan then operation. */
|
|
204
212
|
operations: OperationTable<Procedures<KayanList, GlobalList, Declared<KayanList, GlobalList>, L>>;
|
|
205
|
-
/** The router the first plug built, which a typed client is built from; `undefined` for `fetchHttp
|
|
213
|
+
/** The router the first plug built, which a typed client is built from; `undefined` for `fetchHttp`. */
|
|
206
214
|
router: Http['router'];
|
|
207
215
|
/** Every target as the language passed it, so `asas.targets.drizzle.db` is drizzle. A screen also carries its projection under `client`. */
|
|
208
216
|
targets: { [Name in keyof L['targets']]: L['targets'][Name] extends {
|
package/dist/asas/build.js
CHANGED
|
@@ -5,10 +5,11 @@ import { holdsVerificationSteps, isLocalAuthTarget, localAuthStrategy } from '..
|
|
|
5
5
|
import { bulkSeals, outputOf, sealOf, storeSeals } from './runner/steps.js';
|
|
6
6
|
import { running } from './runner/runtime.js';
|
|
7
7
|
import { tenancy } from './runner/tenant.js';
|
|
8
|
-
import { assertWhere, defaultAccess, defaultAuthorize, projectReadableFields } from './runner/write.js';
|
|
8
|
+
import { assertWhere, defaultAccess, defaultAuthorize, projectReadableFields, referencingIds, restrictingPointers } from './runner/write.js';
|
|
9
9
|
import { combineWhere } from '../mithaq/where.js';
|
|
10
10
|
import { bulk, read as readers, write } from '../service/index.js';
|
|
11
11
|
import { invalid } from './runner/errors.js';
|
|
12
|
+
import { assertMaxBodyBytes, boundedBody } from './http/fetch.js';
|
|
12
13
|
import { fieldlessOperation, keepOperationKeys } from '../rbac/steps.js';
|
|
13
14
|
import { resolvedKayans } from '../mithaq/types.js';
|
|
14
15
|
import { accessTargetOf, isAccess, isScreen, isService, isStorage, isStore, isTenant, isVersions, serviceKeys, serviceTargetOf } from '../mithaq/language.js';
|
|
@@ -16,6 +17,15 @@ import { screensOf } from '../screen/index.js';
|
|
|
16
17
|
import { messagesOf } from '../mithaq/messages.js';
|
|
17
18
|
import { resolve } from '../mithaq/resolve.js';
|
|
18
19
|
import { recordStorage, resolveStorage, storageRoutes } from '../storage/storage.js';
|
|
20
|
+
const lowerCase = (value) => typeof value === 'string' ? value.toLowerCase() : Array.isArray(value) ? value.map(lowerCase) : value;
|
|
21
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
// The parse lower-cases a uuid, as Postgres answers one, but access and the steps before the
|
|
23
|
+
// transaction run ahead of it, so a JS comparison there must see the same spelling.
|
|
24
|
+
const lowerUuids = ({ kayan, input, bulk }) => {
|
|
25
|
+
const fields = (data) => Object.fromEntries(Object.entries(data).map(([key, value]) => [key, kayan.fields[key]?.kind === 'uuid' || kayan.fields[key]?.target?.fields.id?.kind === 'uuid' ? lowerCase(value) : value]));
|
|
26
|
+
// A bulk write's own keys are its query; a row's columns are under `data`.
|
|
27
|
+
return !isObject(input) ? input : bulk ? { ...input, ...(isObject(input.data) && { data: fields(input.data) }) } : fields(input);
|
|
28
|
+
};
|
|
19
29
|
// A symbol: no schema of something a browser can send takes it, so a schema that names
|
|
20
30
|
// the key answers with an issue at the key.
|
|
21
31
|
const probe = Symbol('mithaq.operation.input');
|
|
@@ -50,13 +60,21 @@ singleUpdate, cap, read, kayans, of, audit, admin, authorize, catalog,
|
|
|
50
60
|
/** True where the runner declares a context, which a served call must then carry. */
|
|
51
61
|
contextDeclared,
|
|
52
62
|
/** The most rows a served read of many may ask for, and one bulk write may match. */
|
|
53
|
-
{ maxLimit, bulkLimit }, tenant
|
|
63
|
+
{ maxLimit, bulkLimit }, tenant,
|
|
64
|
+
/** The fields of every kayan that keep a row of this one while they name it. */
|
|
65
|
+
pointers) => {
|
|
54
66
|
// Bound, so a step cannot reach `transaction` or the adapter's own client, and sealed, so no
|
|
55
67
|
// step writes a tenant-owned row outside the active organization.
|
|
68
|
+
const missing = ['upsert', 'sum'].filter((word) => typeof store[word] !== 'function');
|
|
69
|
+
if (missing.length)
|
|
70
|
+
throw new Error(`The store is missing required words: ${missing.join(', ')}.`);
|
|
56
71
|
const writes = tenant.seal({
|
|
72
|
+
find: store.find.bind(store),
|
|
57
73
|
insert: store.insert.bind(store),
|
|
58
74
|
remove: store.remove.bind(store),
|
|
59
75
|
update: store.update.bind(store),
|
|
76
|
+
upsert: store.upsert.bind(store),
|
|
77
|
+
sum: store.sum.bind(store),
|
|
60
78
|
});
|
|
61
79
|
const table = [];
|
|
62
80
|
/** The bulk lists, held for the second pass: each needs a method the first builds. */
|
|
@@ -64,8 +82,10 @@ contextDeclared,
|
|
|
64
82
|
// Options come from the plug alone, so nothing a client sends reaches a step as one.
|
|
65
83
|
// On a runner that declares a context, a plug carrying none is refused before any step.
|
|
66
84
|
const publish = (name, effect, input, method, bounded = false) => {
|
|
85
|
+
const bulk = kayan.operations[name].steps.some((step) => bulkSeals.includes(sealOf(step)));
|
|
67
86
|
// The declared arity is what this widens across, which TypeScript asks two steps for.
|
|
68
|
-
const
|
|
87
|
+
const normalized = ((given, options) => method(lowerUuids({ kayan, input: given, bulk }), options));
|
|
88
|
+
const called = normalized;
|
|
69
89
|
table.push({
|
|
70
90
|
kayan: kayan.name,
|
|
71
91
|
name,
|
|
@@ -74,7 +94,7 @@ contextDeclared,
|
|
|
74
94
|
input,
|
|
75
95
|
call: (given, { actor, context, draft, autosave, tenant }) => {
|
|
76
96
|
if (contextDeclared && !context) {
|
|
77
|
-
throw new Error(`Kayan "${kayan.name}" operation "${name}" was served with no context, and this build's runner declares one. Give every http plug a context function: fetchHttp({ context }) or orpcHttp
|
|
97
|
+
throw new Error(`Kayan "${kayan.name}" operation "${name}" was served with no context, and this build's runner declares one. Give every http plug a context function: fetchHttp({ context, maxBodyBytes: defaultMaxBodyBytes, onError: console.error }), or orpcHttp with the same options.`);
|
|
78
98
|
}
|
|
79
99
|
if (bounded && given && typeof given === 'object' && 'limit' in given) {
|
|
80
100
|
const limit = given.limit;
|
|
@@ -85,7 +105,7 @@ contextDeclared,
|
|
|
85
105
|
return called(given, { actor, ...(context ? { context } : {}), ...(draft ? { draft } : {}), ...(autosave ? { autosave } : {}), ...(tenant !== undefined ? { tenant } : {}) });
|
|
86
106
|
},
|
|
87
107
|
});
|
|
88
|
-
return
|
|
108
|
+
return normalized;
|
|
89
109
|
};
|
|
90
110
|
const methods = Object.create(null);
|
|
91
111
|
for (const [operation, declaration] of Object.entries(kayan.operations)) {
|
|
@@ -145,16 +165,20 @@ contextDeclared,
|
|
|
145
165
|
enumerable: true,
|
|
146
166
|
get: () => kayans(declaration.effect),
|
|
147
167
|
});
|
|
168
|
+
if (declaration.each && !bulkSeals.includes(kind))
|
|
169
|
+
throw new Error(`Kayan "${kayan.name}" operation "${operation}" names "each", and only a bulk operation runs another on each row.`);
|
|
148
170
|
// Built in the second pass, where every single-row write it delegates to exists.
|
|
149
171
|
if (bulkSeals.includes(kind)) {
|
|
150
172
|
bulkLists.push({ operation, declaration, tools, input });
|
|
151
173
|
continue;
|
|
152
174
|
}
|
|
153
|
-
const run = write({ kayan, declaration, tools, store, cap, authorize, admin, tenantOf: tenant.active });
|
|
175
|
+
const run = write({ kayan, declaration, tools, store, cap, authorize, admin, tenantOf: tenant.active, pointers });
|
|
154
176
|
methods[operation] = publish(operation, declaration.effect, input, run);
|
|
155
177
|
}
|
|
156
178
|
for (const { operation, declaration, tools, input } of bulkLists) {
|
|
157
|
-
const single = declaration.effect === 'update' ? 'update' : 'delete';
|
|
179
|
+
const single = declaration.each ?? (declaration.effect === 'update' ? 'update' : 'delete');
|
|
180
|
+
if (!methods[single])
|
|
181
|
+
throw new Error(`Kayan "${kayan.name}" operation "${operation}" runs "${single}" on each row it matches, and the kayan has no operation "${single}".`);
|
|
158
182
|
const run = bulk({
|
|
159
183
|
kayan,
|
|
160
184
|
declaration,
|
|
@@ -202,7 +226,7 @@ const refuseArkan = (language) => {
|
|
|
202
226
|
export const buildAsas = (language, { kayan, global, auth, http, messages, statements: declaredStatements, }) => {
|
|
203
227
|
// Refused before any work: core picks no plug of its own.
|
|
204
228
|
if (!http?.length) {
|
|
205
|
-
throw new Error('buildAsas was handed no "http", and core picks no plug of its own. Name the plugs: http: [fetchHttp()], or
|
|
229
|
+
throw new Error('buildAsas was handed no "http", and core picks no plug of its own. Name the plugs: http: [fetchHttp({ maxBodyBytes: defaultMaxBodyBytes, onError: console.error })], or orpcHttp with the same options first for the typed client.');
|
|
206
230
|
}
|
|
207
231
|
const globals = (global ?? []);
|
|
208
232
|
const globalNames = new Set(globals.map((one) => one.name));
|
|
@@ -420,6 +444,20 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
420
444
|
throw new Error(`Kayan "${betterAuthKayan.join('" and "')}" each carry Better Auth, and kayan "${owned.name}" belongs to the organization a session names. A build with a tenant-owned kayan carries one Better Auth kayan.`);
|
|
421
445
|
}
|
|
422
446
|
const tenant = tenancy({ language, assigned, onError: (error) => auth?.onError?.(error), collection: betterAuthKayan[0] });
|
|
447
|
+
// A project of one organization that came to hold two would split its people and rows between them
|
|
448
|
+
// without a word, so it does not start. Two rows are read, not counted, so every store can answer.
|
|
449
|
+
const refuseExtraTenants = async () => {
|
|
450
|
+
const target = language.targets.tenant;
|
|
451
|
+
if (!isTenant(target) || target.organizations === 'many')
|
|
452
|
+
return;
|
|
453
|
+
const tenants = resolvedByName.get(target.relationTo);
|
|
454
|
+
if (!tenants)
|
|
455
|
+
return;
|
|
456
|
+
const found = await buildTargets[tenants.store].find({ kayan: tenants.name, args: { limit: 2, page: 1, sort: { field: 'id', direction: 'asc' } } });
|
|
457
|
+
if (found.docs.length < 2)
|
|
458
|
+
return;
|
|
459
|
+
throw new Error(`The tenant target says organizations: '${target.organizations}', and the "${tenants.name}" table holds more than one. Set organizations to 'many' where the project sets it, or merge them into one first; a merge is a migration of the project's own.`);
|
|
460
|
+
};
|
|
423
461
|
// The tenant scope goes under both authorizers, so replacing either cannot leave it out.
|
|
424
462
|
const authorize = tenant.scoped(service.authorize === defaultAuthorize
|
|
425
463
|
? defaultAuthorize
|
|
@@ -509,12 +547,16 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
509
547
|
admin,
|
|
510
548
|
authorize,
|
|
511
549
|
openTenant: tenant.open,
|
|
550
|
+
tenantOf: tenant.active,
|
|
512
551
|
kayans: () => boundKayans('read'),
|
|
513
552
|
of: boundOf('read'),
|
|
514
553
|
audit: service.audit,
|
|
515
554
|
catalog,
|
|
555
|
+
pickers: Object.fromEntries(resolved.flatMap((other) => Object.values(other.fields)
|
|
556
|
+
.filter((field) => field.kind !== 'join' && field.target?.name === one.name)
|
|
557
|
+
.map((field) => [`${other.name}.${field.name}`, field]))),
|
|
516
558
|
});
|
|
517
|
-
const { methods, table } = operations(one, store, schemas, singleUpdate, cap, read, (effect) => boundKayans(effect === 'read' ? 'read' : 'write'), (effect) => boundOf(effect === 'read' ? 'read' : 'write'), service.audit, admin, authorize, catalog, service.context !== undefined, service, { active: tenant.active, seal: (writes) => tenant.sealWrites({ writes, find: store.find.bind(store), kayanOf: (name) => resolvedByName.get(name) }) });
|
|
559
|
+
const { methods, table } = operations(one, store, schemas, singleUpdate, cap, read, (effect) => boundKayans(effect === 'read' ? 'read' : 'write'), (effect) => boundOf(effect === 'read' ? 'read' : 'write'), service.audit, admin, authorize, catalog, service.context !== undefined, service, { active: tenant.active, seal: (writes) => tenant.sealWrites({ writes, find: store.find.bind(store), kayanOf: (name) => resolvedByName.get(name) }) }, restrictingPointers(resolved, one));
|
|
518
560
|
operationTable.push(...table);
|
|
519
561
|
const bound = Object.freeze(methods);
|
|
520
562
|
// Only what this kayan's own targets built for it; their configuration stays on `asas.targets`.
|
|
@@ -539,7 +581,7 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
539
581
|
.filter((operation) => operation.kayan === one.name && operation.effect === effect)
|
|
540
582
|
.map((operation) => operation.name);
|
|
541
583
|
const operations = {};
|
|
542
|
-
//
|
|
584
|
+
// Backoffice staff in the backoffice app read a tenant-owned kayan across every organization,
|
|
543
585
|
// so its read is reported as they make it; anyone else is answered in the active one.
|
|
544
586
|
const reach = effect === 'read' && one.tenant && !one.single
|
|
545
587
|
? await tenant.open({ actor: actorForAccess, reach: 'all', kayan: one }).catch(() => undefined)
|
|
@@ -573,15 +615,24 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
573
615
|
};
|
|
574
616
|
// Precedence is the order the build lists its kayan: the loop takes the first non-null actor.
|
|
575
617
|
if (authenticating.length && typeof auth?.onError !== 'function') {
|
|
576
|
-
throw new Error(`buildAsas was handed no "auth.onError", and kayan "${authenticating[0].name}" carries auth. Core reports no failed strategy of its own: name the project's own, such as auth: { onError: console.error }.`);
|
|
618
|
+
throw new Error(`buildAsas was handed no "auth.onError", and kayan "${authenticating[0].name}" carries auth. Core reports no failed strategy of its own: name the project's own, such as auth: { onError: console.error, maxBodyBytes: defaultMaxBodyBytes }.`);
|
|
577
619
|
}
|
|
620
|
+
if (authenticating.length)
|
|
621
|
+
assertMaxBodyBytes('auth', auth.maxBodyBytes);
|
|
578
622
|
const onError = (error) => auth.onError(error);
|
|
579
623
|
const strategies = (() => {
|
|
580
624
|
const list = [];
|
|
581
625
|
// Without this freeze a `roles` function reassigning `v.catalog` would be seen by a
|
|
582
626
|
// `server` factory, and the target and the provider would disagree.
|
|
583
|
-
const vocabulary = Object.freeze({ catalog: catalog, statements });
|
|
584
627
|
for (const one of authenticating) {
|
|
628
|
+
const own = resolvedByName.get(one.name);
|
|
629
|
+
const pointers = restrictingPointers(resolved, own);
|
|
630
|
+
const ownStore = buildTargets[own.store];
|
|
631
|
+
const vocabulary = Object.freeze({
|
|
632
|
+
catalog: catalog,
|
|
633
|
+
statements,
|
|
634
|
+
referenced: async (id) => [...(await referencingIds({ id, pointers, store: ownStore })).values()].reduce((sum, ids) => sum + ids.size, 0),
|
|
635
|
+
});
|
|
585
636
|
const declaration = one.auth;
|
|
586
637
|
if (!isAuthDeclaration(declaration)) {
|
|
587
638
|
list.push(declaration);
|
|
@@ -672,7 +723,7 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
672
723
|
statements: statements,
|
|
673
724
|
/** Every built operation, flat. Its `procedures` view is a type, keyed by kayan then operation. */
|
|
674
725
|
operations: operationTable,
|
|
675
|
-
/** The router the first plug built, which a typed client is built from; `undefined` for `fetchHttp
|
|
726
|
+
/** The router the first plug built, which a typed client is built from; `undefined` for `fetchHttp`. */
|
|
676
727
|
router: plugs[0]?.router,
|
|
677
728
|
/** Every target as the language passed it, so `asas.targets.drizzle.db` is drizzle. A screen also carries its projection under `client`. */
|
|
678
729
|
targets: Object.fromEntries(Object.entries(buildTargets).map(([name, target]) => [name, isScreen(target) ? { ...target, client: screens[name] } : target])),
|
|
@@ -697,15 +748,9 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
697
748
|
// logged by the provider as its own 500.
|
|
698
749
|
if (!request.body)
|
|
699
750
|
return strategy.handler(request);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
body
|
|
703
|
-
}
|
|
704
|
-
catch (thrown) {
|
|
705
|
-
if (request.signal.aborted)
|
|
706
|
-
return new Response(null, { status: 499 });
|
|
707
|
-
throw thrown;
|
|
708
|
-
}
|
|
751
|
+
const body = await boundedBody(request, auth.maxBodyBytes);
|
|
752
|
+
if (body instanceof Response)
|
|
753
|
+
return body;
|
|
709
754
|
return strategy.handler(new Request(request.url, { method: request.method, headers: request.headers, body, signal: request.signal }));
|
|
710
755
|
}
|
|
711
756
|
}
|
|
@@ -735,6 +780,8 @@ export const buildAsas = (language, { kayan, global, auth, http, messages, state
|
|
|
735
780
|
// `readOnly` writes nothing: no push.
|
|
736
781
|
for (const [, store] of stores)
|
|
737
782
|
await store.connect({ readOnly });
|
|
783
|
+
if (!readOnly)
|
|
784
|
+
await refuseExtraTenants();
|
|
738
785
|
},
|
|
739
786
|
destroy: async () => {
|
|
740
787
|
for (const [, store] of stores)
|
package/dist/asas/cli/mithaq.js
CHANGED
|
@@ -10,13 +10,36 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
// Each generator is imported when its word is asked for, so `better-auth/db` loads
|
|
11
11
|
// only for `generate auth` and the `generate --check` sweep.
|
|
12
12
|
import { realpathSync } from 'node:fs';
|
|
13
|
+
import * as modules from 'node:module';
|
|
13
14
|
import { pathToFileURL } from 'node:url';
|
|
14
15
|
import { Refusal } from '../../mithaq/refusal.js';
|
|
15
|
-
const verbs = ['generate', 'lock', 'migrate', 'screens', 'upgrade'];
|
|
16
|
+
const verbs = ['eject', 'generate', 'lock', 'migrate', 'screens', 'upgrade'];
|
|
16
17
|
const nouns = ['auth', 'messages', 'screens'];
|
|
17
|
-
const usage = 'Usage: mithaq generate auth [--check] | mithaq generate messages [--check] [--no-data-scan] | mithaq generate screens [--check] | mithaq generate --check | mithaq lock [--check] | mithaq migrate [--check] [--name <slug>] | mithaq screens | mithaq upgrade --check | mithaq <target> <command> [--check]';
|
|
18
|
+
const usage = 'Usage: mithaq generate auth [--check] | mithaq generate messages [--check] [--no-data-scan] | mithaq generate screens [--check] | mithaq generate --check | mithaq lock [--check] | mithaq migrate [--check] [--name <slug>] | mithaq screens | mithaq upgrade --check | mithaq eject <item> | mithaq eject --prune | mithaq <target> <command> [--check]';
|
|
18
19
|
// Run as a command, not imported. The bin is a symlink, so compare real paths.
|
|
19
20
|
if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
21
|
+
// A project's sources may import each other with no extension, as Vite, Bun and drizzle-kit
|
|
22
|
+
// resolve them and Node does not, so a relative import that names no file is tried as `.ts`,
|
|
23
|
+
// then as a folder's `index.ts`. Only after Node's own resolution failed, never under node_modules.
|
|
24
|
+
modules.registerHooks?.({
|
|
25
|
+
resolve: (specifier, context, next) => {
|
|
26
|
+
try {
|
|
27
|
+
return next(specifier, context);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const code = error.code;
|
|
31
|
+
if (!/^\.\.?\//.test(specifier) || context.parentURL?.includes('/node_modules/') || (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'ERR_UNSUPPORTED_DIR_IMPORT'))
|
|
32
|
+
throw error;
|
|
33
|
+
for (const tail of ['.ts', '/index.ts']) {
|
|
34
|
+
try {
|
|
35
|
+
return next(`${specifier}${tail}`, context);
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
20
43
|
try {
|
|
21
44
|
const [, , verb, noun, ...rest] = process.argv;
|
|
22
45
|
// Before anything reads the project: outside one there is no `src/asas.ts` to import.
|
|
@@ -31,7 +54,10 @@ if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
|
31
54
|
// and never a second word; a target named `migrate` is reached by no spelling.
|
|
32
55
|
const misplaced = (verb === 'migrate' || verb === 'upgrade') && noun !== undefined && !noun.startsWith('--');
|
|
33
56
|
const unchecked = verb === 'upgrade' && !flags.includes('--check');
|
|
34
|
-
|
|
57
|
+
// `eject` takes one item or `--prune`, never both and never neither.
|
|
58
|
+
const ejecting = noun !== undefined && !noun.startsWith('--');
|
|
59
|
+
const unejected = verb === 'eject' && (ejecting ? flags.length > 0 : flags.join(' ') !== '--prune');
|
|
60
|
+
if (verb === undefined || misplaced || unchecked || unejected || (verb === 'generate' && !(sweeping || nouns.includes(noun ?? '')))) {
|
|
35
61
|
console.error(usage);
|
|
36
62
|
process.exit(1);
|
|
37
63
|
}
|
|
@@ -88,6 +114,12 @@ if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
|
88
114
|
if (code)
|
|
89
115
|
process.exit(code);
|
|
90
116
|
}
|
|
117
|
+
else if (verb === 'eject') {
|
|
118
|
+
const { eject, prune } = await import('./upgrade.js');
|
|
119
|
+
const code = ejecting ? await eject(project, noun) : await prune(project);
|
|
120
|
+
if (code)
|
|
121
|
+
process.exit(code);
|
|
122
|
+
}
|
|
91
123
|
else if (verb === 'upgrade') {
|
|
92
124
|
const code = await upgrade();
|
|
93
125
|
if (code)
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
/** The hash of a
|
|
1
|
+
/** The hash of a copy `shadcn add` made, without its marker and the leading comments it dropped. */
|
|
2
2
|
export declare const hashOf: (text: string) => string;
|
|
3
|
-
/**
|
|
4
|
-
export declare const
|
|
3
|
+
/** A held file's state against the package's text of it, read as `upgrade --check` reads it. */
|
|
4
|
+
export declare const stateOf: (text: string, source: string) => 'up to date' | 'up to date, edited' | 'differs' | 'differs, edited' | 'unknown';
|
|
5
5
|
export type Copied = {
|
|
6
|
-
/** The file, relative to the project, or the target or
|
|
6
|
+
/** The file, relative to the project, or the target or package path that resolved outside it. */
|
|
7
7
|
path: string;
|
|
8
8
|
state: 'up to date' | 'up to date, edited' | 'differs' | 'differs, edited' | 'unknown' | 'outside' | 'removed';
|
|
9
|
-
/** The commit the
|
|
9
|
+
/** The version or commit the file's marker names, when it carries one. */
|
|
10
10
|
from?: string;
|
|
11
|
-
/** The
|
|
11
|
+
/** The installed mithaq-admin's version. */
|
|
12
12
|
to?: string;
|
|
13
|
+
/** The item of a file the project always holds, which it never deletes: it ejects the item again instead. */
|
|
14
|
+
item?: string;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
|
-
* Every
|
|
16
|
-
* a path that climbs out of the project or the
|
|
17
|
+
* Every package file the project holds at its target, ejected or always the project's, with its state. Reads only;
|
|
18
|
+
* a path that climbs out of the project or the package is listed as outside, never followed.
|
|
17
19
|
*/
|
|
18
20
|
export declare const upgradeCheck: (project: URL) => Promise<Copied[] | undefined>;
|
|
19
|
-
/** `mithaq upgrade --check`: one line per
|
|
21
|
+
/** `mithaq upgrade --check`: one line per file the project holds, and 1 where any differs. It never writes. */
|
|
20
22
|
export declare const run: (project: URL) => Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* `mithaq eject <item>`: writes the item's own files where the project keeps them, each byte for byte with a marker,
|
|
25
|
+
* and never over a file already there. It copies no other item and installs nothing: the package serves every
|
|
26
|
+
* sibling, so it only names a primitive or an npm package the project lacks.
|
|
27
|
+
*/
|
|
28
|
+
export declare const eject: (project: URL, name: string) => Promise<number>;
|
|
29
|
+
/**
|
|
30
|
+
* `mithaq eject --prune`: deletes every marked file in the folders pieces are ejected into whose text still hashes
|
|
31
|
+
* to its marker, so the package draws it again. Never an edited, unmarked or `~/` file.
|
|
32
|
+
*/
|
|
33
|
+
export declare const prune: (project: URL) => Promise<number>;
|