@drhomet/mithaq 0.0.0-stage → 0.1.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.
Files changed (151) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/dist/asas/actor.d.ts +96 -0
  5. package/dist/asas/actor.js +114 -0
  6. package/dist/asas/build.d.ts +231 -0
  7. package/dist/asas/build.js +787 -0
  8. package/dist/asas/cli/mithaq.d.ts +2 -0
  9. package/dist/asas/cli/mithaq.js +145 -0
  10. package/dist/asas/cli/upgrade.d.ts +20 -0
  11. package/dist/asas/cli/upgrade.js +147 -0
  12. package/dist/asas/http/fetch.d.ts +23 -0
  13. package/dist/asas/http/fetch.js +115 -0
  14. package/dist/asas/http/orpc.d.ts +43 -0
  15. package/dist/asas/http/orpc.js +135 -0
  16. package/dist/asas/runner/bulk.d.ts +20 -0
  17. package/dist/asas/runner/bulk.js +191 -0
  18. package/dist/asas/runner/errors.d.ts +60 -0
  19. package/dist/asas/runner/errors.js +59 -0
  20. package/dist/asas/runner/read.d.ts +68 -0
  21. package/dist/asas/runner/read.js +579 -0
  22. package/dist/asas/runner/runtime.d.ts +21 -0
  23. package/dist/asas/runner/runtime.js +8 -0
  24. package/dist/asas/runner/steps.d.ts +400 -0
  25. package/dist/asas/runner/steps.js +271 -0
  26. package/dist/asas/runner/tenant.d.ts +54 -0
  27. package/dist/asas/runner/tenant.js +213 -0
  28. package/dist/asas/runner/write.d.ts +444 -0
  29. package/dist/asas/runner/write.js +791 -0
  30. package/dist/auth/better-auth/access.d.ts +15 -0
  31. package/dist/auth/better-auth/access.js +71 -0
  32. package/dist/auth/better-auth/generate.d.ts +29 -0
  33. package/dist/auth/better-auth/generate.js +310 -0
  34. package/dist/auth/better-auth/index.d.ts +92 -0
  35. package/dist/auth/better-auth/index.js +81 -0
  36. package/dist/auth/better-auth/tenant.d.ts +18 -0
  37. package/dist/auth/better-auth/tenant.js +26 -0
  38. package/dist/auth/local/index.d.ts +285 -0
  39. package/dist/auth/local/index.js +736 -0
  40. package/dist/base/decimal.d.ts +9 -0
  41. package/dist/base/decimal.js +16 -0
  42. package/dist/base/fields.d.ts +123 -0
  43. package/dist/base/fields.js +51 -0
  44. package/dist/base/index.d.ts +46 -0
  45. package/dist/base/index.js +5 -0
  46. package/dist/base/kinds.d.ts +54 -0
  47. package/dist/base/kinds.js +78 -0
  48. package/dist/base/rules.d.ts +73 -0
  49. package/dist/base/rules.js +80 -0
  50. package/dist/drizzle/index.d.ts +438 -0
  51. package/dist/drizzle/index.js +832 -0
  52. package/dist/drizzle/kinds.d.ts +27 -0
  53. package/dist/drizzle/kinds.js +41 -0
  54. package/dist/drizzle/migrate.d.ts +133 -0
  55. package/dist/drizzle/migrate.js +1285 -0
  56. package/dist/drizzle/proxy/index.d.ts +1 -0
  57. package/dist/drizzle/proxy/index.js +1 -0
  58. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  59. package/dist/drizzle/proxy/node-postgres.js +1 -0
  60. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  61. package/dist/drizzle/proxy/pg-core.js +1 -0
  62. package/dist/drizzle/rules.d.ts +336 -0
  63. package/dist/drizzle/rules.js +44 -0
  64. package/dist/index.d.ts +45 -0
  65. package/dist/index.js +36 -0
  66. package/dist/mithaq/define.d.ts +288 -0
  67. package/dist/mithaq/define.js +43 -0
  68. package/dist/mithaq/generate-messages.d.ts +38 -0
  69. package/dist/mithaq/generate-messages.js +234 -0
  70. package/dist/mithaq/language.d.ts +335 -0
  71. package/dist/mithaq/language.js +76 -0
  72. package/dist/mithaq/lock.d.ts +71 -0
  73. package/dist/mithaq/lock.js +329 -0
  74. package/dist/mithaq/messages.d.ts +55 -0
  75. package/dist/mithaq/messages.js +106 -0
  76. package/dist/mithaq/refusal.d.ts +6 -0
  77. package/dist/mithaq/refusal.js +6 -0
  78. package/dist/mithaq/resolve.d.ts +10 -0
  79. package/dist/mithaq/resolve.js +954 -0
  80. package/dist/mithaq/rows.d.ts +87 -0
  81. package/dist/mithaq/rows.js +174 -0
  82. package/dist/mithaq/rukn.d.ts +72 -0
  83. package/dist/mithaq/rukn.js +15 -0
  84. package/dist/mithaq/standard-schema.d.ts +38 -0
  85. package/dist/mithaq/standard-schema.js +2 -0
  86. package/dist/mithaq/types.d.ts +358 -0
  87. package/dist/mithaq/types.js +15 -0
  88. package/dist/mithaq/user.d.ts +19 -0
  89. package/dist/mithaq/user.js +16 -0
  90. package/dist/mithaq/where.d.ts +32 -0
  91. package/dist/mithaq/where.js +87 -0
  92. package/dist/rbac/index.d.ts +105 -0
  93. package/dist/rbac/index.js +182 -0
  94. package/dist/rbac/steps.d.ts +22 -0
  95. package/dist/rbac/steps.js +161 -0
  96. package/dist/rich-text/index.d.ts +27 -0
  97. package/dist/rich-text/index.js +58 -0
  98. package/dist/rich-text/rich-text.d.ts +159 -0
  99. package/dist/rich-text/rich-text.js +372 -0
  100. package/dist/screen/client.d.ts +153 -0
  101. package/dist/screen/client.js +68 -0
  102. package/dist/screen/generate.d.ts +14 -0
  103. package/dist/screen/generate.js +226 -0
  104. package/dist/screen/index.d.ts +479 -0
  105. package/dist/screen/index.js +645 -0
  106. package/dist/screen/react.d.ts +95 -0
  107. package/dist/screen/react.js +58 -0
  108. package/dist/screen/studio.d.ts +24 -0
  109. package/dist/screen/studio.js +68 -0
  110. package/dist/service/index.d.ts +63 -0
  111. package/dist/service/index.js +15 -0
  112. package/dist/storage/allowed-type.d.ts +5 -0
  113. package/dist/storage/allowed-type.js +7 -0
  114. package/dist/storage/index.d.ts +43 -0
  115. package/dist/storage/index.js +44 -0
  116. package/dist/storage/storage.d.ts +384 -0
  117. package/dist/storage/storage.js +405 -0
  118. package/dist/versions/index.d.ts +37 -0
  119. package/dist/versions/index.js +8 -0
  120. package/dist/versions/versions.d.ts +104 -0
  121. package/dist/versions/versions.js +160 -0
  122. package/dist/zod/index.d.ts +184 -0
  123. package/dist/zod/index.js +121 -0
  124. package/dist/zod/kinds.d.ts +34 -0
  125. package/dist/zod/kinds.js +49 -0
  126. package/dist/zod/rules.d.ts +117 -0
  127. package/dist/zod/rules.js +67 -0
  128. package/package.json +63 -4
  129. package/r/mithaq-cells.json +20 -0
  130. package/r/mithaq-controls.json +37 -0
  131. package/r/mithaq-directional-icon.json +15 -0
  132. package/r/mithaq-expected-updated-at.json +15 -0
  133. package/r/mithaq-field-shell.json +18 -0
  134. package/r/mithaq-filter-operators.json +15 -0
  135. package/r/mithaq-form.json +33 -0
  136. package/r/mithaq-list-columns.json +23 -0
  137. package/r/mithaq-list-filters.json +24 -0
  138. package/r/mithaq-list-pagination.json +24 -0
  139. package/r/mithaq-list-query.json +18 -0
  140. package/r/mithaq-list-selection.json +19 -0
  141. package/r/mithaq-list.json +33 -0
  142. package/r/mithaq-nav.json +24 -0
  143. package/r/mithaq-nodes.json +34 -0
  144. package/r/mithaq-relationship-cell.json +20 -0
  145. package/r/mithaq-relationship-control.json +20 -0
  146. package/r/mithaq-rich-text.json +40 -0
  147. package/r/mithaq-screens-host.json +96 -0
  148. package/r/mithaq-storage.json +44 -0
  149. package/r/mithaq-upload.json +29 -0
  150. package/r/registry.json +550 -0
  151. package/README.md +0 -3
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
10
+ // Each generator is imported when its word is asked for, so `better-auth/db` loads
11
+ // only for `generate auth` and the `generate --check` sweep.
12
+ import { realpathSync } from 'node:fs';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { Refusal } from '../../mithaq/refusal.js';
15
+ const verbs = ['generate', 'lock', 'migrate', 'screens', 'upgrade'];
16
+ 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
+ // Run as a command, not imported. The bin is a symlink, so compare real paths.
19
+ if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
20
+ try {
21
+ const [, , verb, noun, ...rest] = process.argv;
22
+ // Before anything reads the project: outside one there is no `src/asas.ts` to import.
23
+ if (verb === '--help' || verb === '-h') {
24
+ console.log(usage);
25
+ process.exit(0);
26
+ }
27
+ // `generate --check` takes no noun, so the word in the noun's place is a flag.
28
+ const flags = noun?.startsWith('--') ? [noun, ...rest] : rest;
29
+ const sweeping = verb === 'generate' && flags.includes('--check') && !nouns.includes(noun ?? '');
30
+ // `migrate` is a word of the bin's own wherever it stands first, so it takes flags
31
+ // and never a second word; a target named `migrate` is reached by no spelling.
32
+ const misplaced = (verb === 'migrate' || verb === 'upgrade') && noun !== undefined && !noun.startsWith('--');
33
+ const unchecked = verb === 'upgrade' && !flags.includes('--check');
34
+ if (verb === undefined || misplaced || unchecked || (verb === 'generate' && !(sweeping || nouns.includes(noun ?? '')))) {
35
+ console.error(usage);
36
+ process.exit(1);
37
+ }
38
+ const project = pathToFileURL(`${process.cwd()}/`);
39
+ const check = flags.includes('--check');
40
+ // The project saying it has no database to scan, which a check otherwise refuses to pass without.
41
+ const scan = !flags.includes('--no-data-scan');
42
+ const auth = async (checking) => (await import('../../auth/better-auth/generate.js')).run(project, checking);
43
+ const messages = async () => (await import('../../mithaq/generate-messages.js')).run({ project, check, dataScan: scan });
44
+ const screens = async () => (await import('../../screen/generate.js')).run(project, check);
45
+ const upgrade = async () => (await import('./upgrade.js')).run(project);
46
+ const lock = async (word, words) => (await import('../../mithaq/lock.js')).run({ project, noun: word, flags: words });
47
+ // Imported only where a target's command was asked for: `generate auth` runs against
48
+ // a project whose `src/asas.ts` may not import at all.
49
+ const asasOf = async () => (await import(__rewriteRelativeImportExtension(new URL('src/asas.ts', project).href))).asas;
50
+ // `Object.hasOwn`: a typed word would otherwise reach `Object.prototype`, and
51
+ // `mithaq drizzle constructor` would call `Object`.
52
+ const commandsOf = (asas, name) => (Object.hasOwn(asas.targets, name) ? asas.targets[name]?.commands : undefined) ?? {};
53
+ const commandIn = (commands, name) => Object.hasOwn(commands, name) ? commands[name] : undefined;
54
+ const everyCommand = (asas) => Object.entries(asas.targets).flatMap(([name, target]) => Object.entries(target?.commands ?? {}).map(([command, one]) => [`${name} ${command}`, one]));
55
+ const stopped = (name, code) => {
56
+ console.error(`\`mithaq ${name} --check\` answered ${code}, so the sweep stopped there.`);
57
+ process.exit(code);
58
+ };
59
+ // Every command but the lock's own and the sweep shows an unsigned language change first.
60
+ if (verb !== 'lock' && !(verb === 'generate' && sweeping))
61
+ await (await import('../../mithaq/lock.js')).warnLock(project);
62
+ if (verb === 'generate' && sweeping) {
63
+ // Stops at the first non-zero, so a pipeline reads which check failed. The lock goes first,
64
+ // and `migrate` before anything that reads rows, so an unmigrated database stops it by name.
65
+ const step = async (name, one) => {
66
+ const code = await one();
67
+ if (code)
68
+ stopped(name, code);
69
+ };
70
+ await step('lock', () => lock(undefined, ['--check']));
71
+ await step('generate auth', () => auth(true));
72
+ // A command with no check to make answers 0 for the flag it does not know.
73
+ const asas = await asasOf();
74
+ const commands = everyCommand(asas);
75
+ const migrates = commands.filter(([name]) => name.endsWith(' migrate'));
76
+ const others = commands.filter(([name]) => !name.endsWith(' migrate'));
77
+ for (const [name, one] of migrates)
78
+ await step(name, () => one({ asas, flags }));
79
+ await step('generate messages', messages);
80
+ await step('generate screens', screens);
81
+ for (const [name, one] of others)
82
+ await step(name, () => one({ asas, flags }));
83
+ // A report, never a gate: a copy that differs from the registry is a file the project owns.
84
+ await upgrade().catch((error) => console.error(`upgrade --check could not read the copies: ${error.message}`));
85
+ }
86
+ else if (verb === 'lock') {
87
+ const code = await lock(noun?.startsWith('--') ? undefined : noun, flags);
88
+ if (code)
89
+ process.exit(code);
90
+ }
91
+ else if (verb === 'upgrade') {
92
+ const code = await upgrade();
93
+ if (code)
94
+ process.exit(code);
95
+ }
96
+ else if (verb === 'generate') {
97
+ const code = await (noun === 'messages' ? messages() : noun === 'screens' ? screens() : auth(check));
98
+ if (code)
99
+ process.exit(code);
100
+ }
101
+ else if (verb === 'screens') {
102
+ const { studioText, studioTree } = await import('../../screen/studio.js');
103
+ const targets = (await asasOf()).targets;
104
+ const trees = Object.entries(targets).flatMap(([name, target]) => (target?.capability === 'screen' ? [studioText(studioTree(name, target.client))] : []));
105
+ console.log(trees.join('\n\n'));
106
+ }
107
+ else {
108
+ const asas = await asasOf();
109
+ // Two stores that both declare `migrate` cannot be told apart by the bare word, so it is refused.
110
+ const aliased = verb === 'migrate';
111
+ const declared = aliased ? Object.keys(asas.targets).filter((name) => commandsOf(asas, name).migrate) : [];
112
+ if (aliased && declared.length === 0) {
113
+ throw new Refusal('No target of this language declares a `migrate` command, and that is what `mithaq migrate` runs. Name a store that declares one in `targets`, or migrate that store with its own tool.');
114
+ }
115
+ if (aliased && declared.length > 1) {
116
+ throw new Refusal(`${declared.join(', ')} each declare a \`migrate\` command, so \`mithaq migrate\` cannot say which one to run. Name the target: ${declared.map((name) => `\`mithaq ${name} migrate\``).join(', ')}.`);
117
+ }
118
+ const target = aliased ? declared[0] : verb;
119
+ const command = aliased ? 'migrate' : noun;
120
+ if (!Object.hasOwn(asas.targets, target)) {
121
+ throw new Refusal(`"${verb}" is neither a command of mithaq nor a target of this language. The commands are ${verbs.join(' and ')}; the targets are ${Object.keys(asas.targets).join(', ')}.`);
122
+ }
123
+ if (command === undefined || command.startsWith('--')) {
124
+ console.error(usage);
125
+ process.exit(1);
126
+ }
127
+ const one = commandIn(commandsOf(asas, target), command);
128
+ if (!one) {
129
+ const has = Object.keys(commandsOf(asas, target)).map((name) => `\`${name}\``);
130
+ throw new Refusal(`Target "${target}" declares no \`${command}\` command. It declares ${has.length ? has.join(', ') : 'none at all'}.`);
131
+ }
132
+ const code = await one({ asas, flags });
133
+ if (code)
134
+ process.exit(code);
135
+ }
136
+ }
137
+ catch (error) {
138
+ // Anything that is not a refusal is rethrown, so Node prints its stack, which is the whole
139
+ // diagnosis of a project's `src/asas.ts` failing to import.
140
+ if (!(error instanceof Refusal))
141
+ throw error;
142
+ console.error(error.message);
143
+ process.exit(1);
144
+ }
145
+ }
@@ -0,0 +1,20 @@
1
+ /** The hash of a file as `shadcn add` writes it, without its marker. */
2
+ export declare const hashOf: (text: string) => string;
3
+ /** Appends the marker to every file of every item `shadcn build` wrote into `built`, the registry's `r/`. */
4
+ export declare const mark: (built: URL) => Promise<void>;
5
+ export type Copied = {
6
+ /** The file, relative to the project, or the target or item name that resolved outside it. */
7
+ path: string;
8
+ state: 'up to date' | 'up to date, edited' | 'differs' | 'differs, edited' | 'unknown' | 'outside' | 'removed';
9
+ /** The commit the copy's marker names, when it carries one. */
10
+ from?: string;
11
+ /** The commit the installed registry was built at. */
12
+ to?: string;
13
+ };
14
+ /**
15
+ * Every registry file the project holds where `shadcn add` writes it, with its state. Reads only;
16
+ * a path that climbs out of the project or the registry is listed as outside, never followed.
17
+ */
18
+ export declare const upgradeCheck: (project: URL) => Promise<Copied[] | undefined>;
19
+ /** `mithaq upgrade --check`: one line per copied file, and 1 where any differs. It never writes. */
20
+ export declare const run: (project: URL) => Promise<number>;
@@ -0,0 +1,147 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ // The last line of every registry file: the commit it was built at and the hash of the
8
+ // file above. Trailing, because `shadcn add` drops a file's leading comments only.
9
+ const marker = /\n\/\/ mithaq-admin (\S+) ([0-9a-f]{64})\n?$/;
10
+ // What `shadcn add` drops, so a hash is the same over the registry's text and the copy's.
11
+ const leading = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/;
12
+ /** The hash of a file as `shadcn add` writes it, without its marker. */
13
+ export const hashOf = (text) => createHash('sha256').update(text.replace(marker, '\n').replace(leading, '')).digest('hex');
14
+ /** Appends the marker to every file of every item `shadcn build` wrote into `built`, the registry's `r/`. */
15
+ export const mark = async (built) => {
16
+ let commit = 'unknown';
17
+ try {
18
+ commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: fileURLToPath(built), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
19
+ }
20
+ catch { }
21
+ for (const name of await readdir(built)) {
22
+ if (!name.endsWith('.json') || name === 'registry.json')
23
+ continue;
24
+ const item = JSON.parse(await readFile(new URL(name, built), 'utf8'));
25
+ for (const file of item.files ?? []) {
26
+ const body = file.content.replace(marker, '\n');
27
+ const text = body.endsWith('\n') ? body : `${body}\n`;
28
+ file.content = `${text}// mithaq-admin ${commit} ${hashOf(text)}\n`;
29
+ }
30
+ await writeFile(new URL(name, built), `${JSON.stringify(item, null, 2)}\n`);
31
+ }
32
+ };
33
+ // Beside `dist/` when published, at the same depth from this file.
34
+ // Beside `dist/` when published; in this repository the apps read the workspace's `mithaq-admin`.
35
+ const registry = new URL('../../../r/', import.meta.url);
36
+ // The installed `mithaq-admin`'s `r/`, else the one this mithaq ships, else none.
37
+ const registryOf = (project) => {
38
+ try {
39
+ return new URL('r/', pathToFileURL(createRequire(new URL('package.json', project)).resolve('mithaq-admin/package.json')));
40
+ }
41
+ catch {
42
+ return existsSync(new URL('registry.json', registry)) ? registry : undefined;
43
+ }
44
+ };
45
+ /**
46
+ * Every registry file the project holds where `shadcn add` writes it, with its state. Reads only;
47
+ * a path that climbs out of the project or the registry is listed as outside, never followed.
48
+ */
49
+ export const upgradeCheck = async (project) => {
50
+ const components = JSON.parse(await readFile(new URL('components.json', project), 'utf8'));
51
+ const built = registryOf(project);
52
+ if (!built)
53
+ return undefined;
54
+ const { items } = JSON.parse(await readFile(new URL('registry.json', built), 'utf8'));
55
+ const src = existsSync(new URL('src/', project)) ? 'src/' : '';
56
+ const pathOf = (target) => {
57
+ if (target.startsWith('~/'))
58
+ return target.slice(2);
59
+ const [, alias = '', rest] = /^@([^/]+)\/(.+)$/.exec(target) ?? [];
60
+ // shadcn resolves an alias through the app's tsconfig `paths`; this reads `@/` as `src/`
61
+ // when the project has one and as its root otherwise, shadcn's own fallback.
62
+ const at = components.aliases?.[alias];
63
+ return at?.startsWith('@/') ? `${src}${at.slice(2)}/${rest}` : undefined;
64
+ };
65
+ const copied = [];
66
+ /** Every path an item names, copied or not, and the folders they sit in, which a file no item names any more is looked for in. */
67
+ const named = new Set();
68
+ const folders = new Set();
69
+ // `new URL` resolves every `..`, encoded or not, so a prefix is the whole test.
70
+ const within = (url, folder) => url.href.startsWith(folder.href);
71
+ for (const { name } of items) {
72
+ const source = new URL(`${name}.json`, built);
73
+ if (!within(source, built)) {
74
+ copied.push({ path: name, state: 'outside' });
75
+ continue;
76
+ }
77
+ const item = JSON.parse(await readFile(source, 'utf8'));
78
+ for (const file of item.files ?? []) {
79
+ const path = pathOf(file.target);
80
+ if (path && !within(new URL(path, project), project)) {
81
+ copied.push({ path: file.target, state: 'outside' });
82
+ continue;
83
+ }
84
+ if (path) {
85
+ named.add(path);
86
+ folders.add(path.slice(0, path.lastIndexOf('/') + 1));
87
+ }
88
+ const text = path && (await readFile(new URL(path, project), 'utf8').catch(() => undefined));
89
+ if (!path || text === undefined)
90
+ continue;
91
+ const [, from, hash] = marker.exec(text) ?? [];
92
+ const to = marker.exec(file.content)?.[1];
93
+ if (!hash) {
94
+ copied.push({ path, state: 'unknown', to });
95
+ continue;
96
+ }
97
+ const differs = hash !== hashOf(file.content);
98
+ const edited = hash !== hashOf(text);
99
+ copied.push({ path, state: `${differs ? 'differs' : 'up to date'}${edited ? ', edited' : ''}`, from, to });
100
+ }
101
+ }
102
+ // A marked file no item names any more: an item the registry dropped, which the project deletes.
103
+ for (const folder of folders) {
104
+ const entries = await readdir(new URL(folder || './', project), { withFileTypes: true }).catch(() => []);
105
+ for (const entry of entries) {
106
+ const path = `${folder}${entry.name}`;
107
+ if (!entry.isFile() || named.has(path))
108
+ continue;
109
+ const from = marker.exec(await readFile(new URL(path, project), 'utf8'))?.[1];
110
+ if (from)
111
+ copied.push({ path, state: 'removed', from });
112
+ }
113
+ }
114
+ return copied;
115
+ };
116
+ const sentence = ({ path, state, from = '', to = 'unknown' }, own) => ({
117
+ 'up to date': `${path} is up to date.`,
118
+ 'up to date, edited': `${path} is up to date, and edited since it was copied.`,
119
+ differs: `${path} differs from ${own ? 'the copy in the registry the installed mithaq ships' : "the installed mithaq-admin's copy"} (built at ${to.slice(0, 12)}), and is not edited since it was copied from ${from.slice(0, 12)}. A hash shows a difference, not which side is newer: diff the two${own ? '' : ', or rebuild mithaq-admin,'} before replacing it.`,
120
+ 'differs, edited': `${path} differs from ${own ? 'the copy in the registry the installed mithaq ships' : "the installed mithaq-admin's copy"} (built at ${to.slice(0, 12)}), and is edited since it was copied from ${from.slice(0, 12)}: diff the two and merge by hand.`,
121
+ unknown: `${path} carries no mithaq-admin marker on its last line, so nothing says what it was copied from.`,
122
+ outside: `${path} resolves outside this project or the registry, so it was not read.`,
123
+ removed: `${path} was copied from mithaq-admin at ${from.slice(0, 12)}, and no item of the installed registry names it any more: it was removed from the registry, so delete it.`,
124
+ })[state];
125
+ /** `mithaq upgrade --check`: one line per copied file, and 1 where any differs. It never writes. */
126
+ export const run = async (project) => {
127
+ if (!existsSync(new URL('components.json', project))) {
128
+ console.log('No components.json here, so nothing was copied with `shadcn add`.');
129
+ return 0;
130
+ }
131
+ const copied = await upgradeCheck(project);
132
+ if (!copied) {
133
+ console.log(`No mithaq-admin registry to check the copies against: none is installed here, and the installed mithaq ships no r/ at ${fileURLToPath(registry)}.`);
134
+ return 0;
135
+ }
136
+ const own = registryOf(project)?.href === registry.href;
137
+ if (!copied.length)
138
+ console.log('Nothing from mithaq-admin here.');
139
+ for (const one of copied)
140
+ console.log(sentence(one, own));
141
+ const differing = copied.filter((one) => one.state.startsWith('differs')).length;
142
+ if (differing) {
143
+ console.error(`${differing} ${differing === 1 ? 'file differs' : 'files differ'} from ${own ? 'the registry the installed mithaq ships' : 'the installed mithaq-admin'}.`);
144
+ return 1;
145
+ }
146
+ return 0;
147
+ };
@@ -0,0 +1,23 @@
1
+ import type { HttpPlug, OperationTable } from '../build.ts';
2
+ import type { StandardSchemaV1 } from '../../mithaq/standard-schema.ts';
3
+ /** The Zod input decides which JSON strings represent dates on either HTTP plug. */
4
+ export declare const decodeJson: (value: unknown, schema: StandardSchemaV1) => unknown;
5
+ export declare const draftOptions: (request: Request) => {
6
+ draft?: boolean | undefined;
7
+ autosave?: boolean | undefined;
8
+ tenant?: string | undefined;
9
+ };
10
+ export type FetchHttpOptions = {
11
+ /**
12
+ * Builds the context every step of a served call reads, from the request and the actor.
13
+ * The plug reads no header or body key into it; a throw is answered 500.
14
+ */
15
+ context?: (request: Request, actor: unknown) => Promise<Record<string, unknown>> | Record<string, unknown>;
16
+ /** Handed every error that is not an `AsasError`, before the 500 goes out with its message replaced. Defaults to `console.error`. */
17
+ onError?: (error: unknown) => void;
18
+ };
19
+ /**
20
+ * The default HTTP plug: `POST /api/<kayan>/<operation>` with a JSON body, over
21
+ * `Request` and `Response` alone. The runner parses each operation's input.
22
+ */
23
+ export declare const fetchHttp: ({ context, onError }?: FetchHttpOptions) => <View>(operations: OperationTable<View>) => HttpPlug;
@@ -0,0 +1,115 @@
1
+ import { AfterCommitError, AsasError, afterCommitHeader, codes } from '../runner/errors.js';
2
+ // The walk enters objects, arrays and the wrappers `unwrap()` opens, and nothing else.
3
+ const datesFromJson = (value, schema) => {
4
+ if (schema.type === 'date' && typeof value === 'string') {
5
+ // Date.parse normalizes February 30, which the declared z.date must refuse.
6
+ const day = value.slice(0, 10);
7
+ if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(value)
8
+ && Number(value.slice(11, 13)) < 24) {
9
+ const parsed = new Date(value);
10
+ const midnight = new Date(`${day}T00:00:00Z`);
11
+ if (!Number.isNaN(parsed.getTime()) && !Number.isNaN(midnight.getTime()) && midnight.toISOString().slice(0, 10) === day)
12
+ return parsed;
13
+ }
14
+ }
15
+ if (['optional', 'nullable', 'default', 'prefault', 'nonoptional', 'readonly'].includes(schema.type ?? '')) {
16
+ return schema.unwrap ? datesFromJson(value, schema.unwrap()) : value;
17
+ }
18
+ if (schema.type === 'array' && Array.isArray(value) && schema.element) {
19
+ return value.map((one) => datesFromJson(one, schema.element));
20
+ }
21
+ if (schema.type === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value) && schema.shape) {
22
+ const result = { ...value };
23
+ for (const [name, child] of Object.entries(schema.shape)) {
24
+ if (Object.hasOwn(result, name))
25
+ result[name] = datesFromJson(result[name], child);
26
+ }
27
+ return result;
28
+ }
29
+ return value;
30
+ };
31
+ /** The Zod input decides which JSON strings represent dates on either HTTP plug. */
32
+ export const decodeJson = (value, schema) => schema['~standard'].vendor === 'zod' ? datesFromJson(value, schema) : value;
33
+ // `?draft=true` saves or reads a draft, and `?autosave=true` beside it lets the
34
+ // save overwrite an autosave. `?tenant=` is a read's reach, which the runner judges.
35
+ export const draftOptions = (request) => {
36
+ const query = new URL(request.url).searchParams;
37
+ return {
38
+ ...(query.get('draft') === 'true' ? { draft: true } : {}),
39
+ ...(query.get('autosave') === 'true' ? { autosave: true } : {}),
40
+ ...(query.has('tenant') ? { tenant: query.get('tenant') } : {}),
41
+ };
42
+ };
43
+ /**
44
+ * The default HTTP plug: `POST /api/<kayan>/<operation>` with a JSON body, over
45
+ * `Request` and `Response` alone. The runner parses each operation's input.
46
+ */
47
+ export const fetchHttp = ({ context, onError = console.error } = {}) => (operations) => {
48
+ const table = new Map(operations.map((one) => [`${one.kayan}/${one.name}`, one]));
49
+ return {
50
+ name: 'fetch',
51
+ // The operation table is the routes, already published as `asas.operations`.
52
+ router: undefined,
53
+ handle: async (request, actor) => {
54
+ if (request.method !== 'POST')
55
+ return null;
56
+ // Two segments under `/api` and no more, so another plug's three-segment prefix
57
+ // falls through. `null`, not a 404, so a host keeps its own routes.
58
+ const [api, kayan, name, ...rest] = new URL(request.url).pathname.split('/').slice(1);
59
+ if (api !== 'api' || !kayan || !name || rest.length > 0)
60
+ return null;
61
+ const operation = table.get(`${kayan}/${name}`);
62
+ if (!operation)
63
+ return null;
64
+ const body = await request.text();
65
+ // Keyless: refused before any operation, and the key names an operation's refusal.
66
+ const undecodable = (message) => Response.json({ code: codes[400], message }, { status: 400 });
67
+ let input = {};
68
+ if (body) {
69
+ try {
70
+ input = JSON.parse(body);
71
+ }
72
+ catch {
73
+ return undecodable('The request body is not JSON.');
74
+ }
75
+ // The runner reads fields off the input, so a body of `null` or `1` would be a
76
+ // 500 a caller provoked; every caller-provoked refusal is a 4xx.
77
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
78
+ return undecodable('The request body is not a JSON object.');
79
+ }
80
+ }
81
+ try {
82
+ // Resolved after the match, so a host's own routes cost no session lookup.
83
+ const decoded = decodeJson(input, operation.input);
84
+ const who = await actor();
85
+ const result = await operation.call(decoded, { actor: who, context: await context?.(request, who), ...draftOptions(request) });
86
+ // Only a write says so, because the browser clears its banner on this value alone.
87
+ return Response.json(result, operation.effect === 'read' ? undefined : { headers: { [afterCommitHeader]: 'ok' } });
88
+ }
89
+ catch (thrown) {
90
+ if (thrown instanceof AfterCommitError) {
91
+ try {
92
+ onError?.(thrown.cause);
93
+ }
94
+ catch {
95
+ // Reporting cannot change a committed result.
96
+ }
97
+ return Response.json(thrown.result, { headers: { [afterCommitHeader]: 'failed' } });
98
+ }
99
+ if (thrown instanceof AsasError) {
100
+ // The cause is never serialized: a conflict's driver error names the column and
101
+ // value the refusal's message withholds.
102
+ return Response.json({ code: codes[thrown.status], data: { key: thrown.key, params: thrown.params }, message: thrown.message }, { status: thrown.status });
103
+ }
104
+ try {
105
+ onError?.(thrown);
106
+ }
107
+ catch {
108
+ // Deliberately empty: reporting may not change what is reported.
109
+ }
110
+ // Not an `AsasError` is the project's own fault, so its message is replaced.
111
+ return Response.json({ code: 'INTERNAL_SERVER_ERROR', message: 'Internal server error' }, { status: 500 });
112
+ }
113
+ },
114
+ };
115
+ };
@@ -0,0 +1,43 @@
1
+ import type { Procedure, Schema } from '@orpc/server';
2
+ import type { OperationTable } from '../build.ts';
3
+ /** The context every procedure is handed, which defaults to the actor alone. */
4
+ export type OrpcContext = {
5
+ actor: unknown;
6
+ } & Record<string, unknown>;
7
+ export type OrpcHttpOptions = {
8
+ /** Where the typed protocol answers. */
9
+ rpc?: `/${string}`;
10
+ /** Where plain HTTP answers, one route per operation. */
11
+ api?: `/${string}`;
12
+ /**
13
+ * Builds the context a procedure receives, once per request. The default is `{ actor }`;
14
+ * the `actor` it returns is the one the call runs as.
15
+ */
16
+ context?: (request: Request, actor: unknown) => Promise<OrpcContext> | OrpcContext;
17
+ /**
18
+ * Handed every error that is not an `AsasError`, before it is rethrown with its message replaced.
19
+ * A host's own deliberate `ORPCError` reaches it too, so a host logging only faults filters on the code.
20
+ */
21
+ onError?: (error: unknown) => void;
22
+ };
23
+ type OrpcProcedure<Method> = Method extends (...args: infer Args) => Promise<infer Answer> ? Procedure<OrpcContext, OrpcContext, Schema<Args[0], Args[0]>, Schema<Answer, Answer>, Record<never, never>, Record<never, never>> : never;
24
+ /** The router, typed from the operation table: kayan, then operation. `RouterClient` over it needs no generated code. */
25
+ export type OrpcRouter<View> = {
26
+ [Kayan in keyof View]: {
27
+ [Name in keyof View[Kayan]]: OrpcProcedure<View[Kayan][Name]>;
28
+ };
29
+ };
30
+ /**
31
+ * The oRPC plug: one procedure per operation, served over both the typed RPC protocol
32
+ * and plain HTTP. Needs `@orpc/server` and `@orpc/openapi`.
33
+ */
34
+ export declare const orpcHttp: ({ rpc, api, context, onError, }?: OrpcHttpOptions) => <View>(operations: OperationTable<View>) => {
35
+ name: string;
36
+ router: OrpcRouter<View>;
37
+ /**
38
+ * `null` when neither prefix matched, so a host keeps its own routes. The path is matched
39
+ * before the actor thunk is called, so a host's own route runs no strategy.
40
+ */
41
+ handle: (request: Request, actor: () => Promise<unknown>) => Promise<Response | null>;
42
+ };
43
+ export {};
@@ -0,0 +1,135 @@
1
+ // The only file that imports oRPC, so it is its own entry and oRPC stays an
2
+ // optional peer a project that serves no HTTP never installs.
3
+ import { OpenAPIHandler } from '@orpc/openapi/fetch';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import { ORPCError, ValidationError, os } from '@orpc/server';
6
+ import { RPCHandler } from '@orpc/server/fetch';
7
+ import { AfterCommitError, AsasError, afterCommitHeader, codes } from '../runner/errors.js';
8
+ import { issueRefusal, withoutPrototype } from '../runner/steps.js';
9
+ import { decodeJson, draftOptions } from './fetch.js';
10
+ // Matched on the `ValidationError` cause, not the shape of `data`, so a host's own
11
+ // `BAD_REQUEST` keeps its message and data.
12
+ const boundaryRefusal = (thrown) => thrown instanceof ORPCError && thrown.code === 'BAD_REQUEST' && thrown.cause instanceof ValidationError
13
+ ? issueRefusal(thrown.cause.issues)
14
+ : undefined;
15
+ /**
16
+ * The oRPC plug: one procedure per operation, served over both the typed RPC protocol
17
+ * and plain HTTP. Needs `@orpc/server` and `@orpc/openapi`.
18
+ */
19
+ export const orpcHttp = ({ rpc = '/api/rpc', api = '/api', context, onError, } = {}) => (operations) => {
20
+ // A reporter that throws must not replace the error the host has to see.
21
+ const report = (thrown) => {
22
+ try {
23
+ onError?.(thrown);
24
+ }
25
+ catch {
26
+ // Deliberately empty: reporting may not change what is reported.
27
+ }
28
+ };
29
+ // Per request, because a procedure's handler cannot read the request itself.
30
+ const warnings = new AsyncLocalStorage();
31
+ // The one place a refusal becomes a wire error, so both handlers share it.
32
+ const base = os.$context().use(async ({ next }) => {
33
+ try {
34
+ return await next();
35
+ }
36
+ catch (thrown) {
37
+ // oRPC validates the bound input inside this middleware, so its own refusal is
38
+ // re-emitted in the runner's shape: one shape answers every 400 a procedure raises.
39
+ const held = thrown instanceof AsasError ? thrown : boundaryRefusal(thrown);
40
+ if (!held) {
41
+ report(thrown);
42
+ throw thrown;
43
+ }
44
+ throw new ORPCError(codes[held.status], {
45
+ message: held.message,
46
+ data: { key: held.key, params: held.params },
47
+ // Never serialize the cause: a conflict's driver error names the column and value
48
+ // on its `detail`. It stays in process only because oRPC's `toJSON` never emits `cause`.
49
+ cause: held,
50
+ });
51
+ }
52
+ });
53
+ // No prototype, so a kayan or operation named `constructor` or `__proto__` is an own key.
54
+ const router = Object.create(null);
55
+ for (const operation of operations) {
56
+ const kayan = (router[operation.kayan] ??= Object.create(null));
57
+ const input = operation.input;
58
+ const zod = input['~standard'].vendor === 'zod';
59
+ // The published shape stays for the OpenAPI contract; only validation is skipped,
60
+ // because the runner parses every input before any store seal, and a draft against the draft shape.
61
+ const wireInput = Object.assign(Object.create(input), {
62
+ '~standard': {
63
+ ...input['~standard'],
64
+ validate: (value, options) => {
65
+ const decoded = zod ? withoutPrototype(decodeJson(value, input)) : value;
66
+ const object = decoded !== null && typeof decoded === 'object' && !Array.isArray(decoded);
67
+ return warnings.getStore()?.options.draft && object ? { value: decoded } : input['~standard'].validate(decoded, options);
68
+ },
69
+ },
70
+ });
71
+ kayan[operation.name] = base
72
+ .input(wireInput)
73
+ .handler(async ({ context: held, input }) => {
74
+ try {
75
+ const { actor, ...rest } = held;
76
+ const result = await operation.call(input, { actor, context: context ? rest : undefined, ...warnings.getStore()?.options });
77
+ const warning = warnings.getStore();
78
+ if (warning && operation.effect !== 'read')
79
+ warning.said = 'ok';
80
+ return result;
81
+ }
82
+ catch (thrown) {
83
+ if (!(thrown instanceof AfterCommitError))
84
+ throw thrown;
85
+ report(thrown.cause);
86
+ const warning = warnings.getStore();
87
+ if (warning)
88
+ warning.said = 'failed';
89
+ return thrown.result;
90
+ }
91
+ });
92
+ }
93
+ const handlers = [
94
+ [rpc, new RPCHandler(router)],
95
+ [api, new OpenAPIHandler(router)],
96
+ ];
97
+ return {
98
+ name: 'orpc',
99
+ router: router,
100
+ /**
101
+ * `null` when neither prefix matched, so a host keeps its own routes. The path is matched
102
+ * before the actor thunk is called, so a host's own route runs no strategy.
103
+ */
104
+ handle: async (request, actor) => {
105
+ const { pathname } = new URL(request.url);
106
+ const under = (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
107
+ if (!handlers.some(([prefix]) => under(prefix)))
108
+ return null;
109
+ // Built before any procedure runs, so a throw here is outside the middleware;
110
+ // it still reaches `onError`.
111
+ let held;
112
+ try {
113
+ const who = await actor();
114
+ held = context ? await context(request, who) : { actor: who };
115
+ }
116
+ catch (thrown) {
117
+ if (!(thrown instanceof AsasError))
118
+ report(thrown);
119
+ throw thrown;
120
+ }
121
+ for (const [prefix, handler] of handlers) {
122
+ if (!under(prefix))
123
+ continue;
124
+ const warning = { options: draftOptions(request) };
125
+ const answered = await warnings.run(warning, () => handler.handle(request, { context: held, prefix }));
126
+ if (answered.matched) {
127
+ if (warning.said)
128
+ answered.response.headers.set(afterCommitHeader, warning.said);
129
+ return answered.response;
130
+ }
131
+ }
132
+ return null;
133
+ },
134
+ };
135
+ };