@drhomet/mithaq 0.0.0-stage → 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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/README.md +160 -2
- package/dist/asas/actor.d.ts +96 -0
- package/dist/asas/actor.js +114 -0
- package/dist/asas/build.d.ts +239 -0
- package/dist/asas/build.js +834 -0
- package/dist/asas/cli/mithaq.d.ts +2 -0
- package/dist/asas/cli/mithaq.js +177 -0
- package/dist/asas/cli/upgrade.d.ts +33 -0
- package/dist/asas/cli/upgrade.js +256 -0
- package/dist/asas/http/fetch.d.ts +41 -0
- package/dist/asas/http/fetch.js +178 -0
- package/dist/asas/http/orpc.d.ts +45 -0
- package/dist/asas/http/orpc.js +153 -0
- package/dist/asas/runner/bulk.d.ts +20 -0
- package/dist/asas/runner/bulk.js +196 -0
- package/dist/asas/runner/errors.d.ts +63 -0
- package/dist/asas/runner/errors.js +75 -0
- package/dist/asas/runner/read.d.ts +72 -0
- package/dist/asas/runner/read.js +601 -0
- package/dist/asas/runner/runtime.d.ts +21 -0
- package/dist/asas/runner/runtime.js +8 -0
- package/dist/asas/runner/steps.d.ts +418 -0
- package/dist/asas/runner/steps.js +277 -0
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +54 -0
- package/dist/asas/runner/tenant.js +275 -0
- package/dist/asas/runner/write.d.ts +606 -0
- package/dist/asas/runner/write.js +1019 -0
- package/dist/auth/better-auth/access.d.ts +29 -0
- package/dist/auth/better-auth/access.js +103 -0
- package/dist/auth/better-auth/generate.d.ts +31 -0
- package/dist/auth/better-auth/generate.js +328 -0
- package/dist/auth/better-auth/index.d.ts +94 -0
- package/dist/auth/better-auth/index.js +81 -0
- package/dist/auth/better-auth/tenant.d.ts +26 -0
- package/dist/auth/better-auth/tenant.js +34 -0
- package/dist/auth/local/index.d.ts +285 -0
- package/dist/auth/local/index.js +738 -0
- package/dist/base/decimal.d.ts +9 -0
- package/dist/base/decimal.js +16 -0
- package/dist/base/fields.d.ts +124 -0
- package/dist/base/fields.js +52 -0
- package/dist/base/index.d.ts +47 -0
- package/dist/base/index.js +5 -0
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +75 -0
- package/dist/base/kinds.js +123 -0
- package/dist/base/rules.d.ts +73 -0
- package/dist/base/rules.js +81 -0
- package/dist/drizzle/index.d.ts +308 -0
- package/dist/drizzle/index.js +951 -0
- package/dist/drizzle/kinds.d.ts +28 -0
- package/dist/drizzle/kinds.js +43 -0
- package/dist/drizzle/migrate.d.ts +159 -0
- package/dist/drizzle/migrate.js +1328 -0
- package/dist/drizzle/proxy/index.d.ts +1 -0
- package/dist/drizzle/proxy/index.js +1 -0
- package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
- package/dist/drizzle/proxy/node-postgres.js +1 -0
- package/dist/drizzle/proxy/pg-core.d.ts +1 -0
- package/dist/drizzle/proxy/pg-core.js +1 -0
- package/dist/drizzle/rules.d.ts +336 -0
- package/dist/drizzle/rules.js +44 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +26 -0
- package/dist/mithaq/define.d.ts +335 -0
- package/dist/mithaq/define.js +57 -0
- package/dist/mithaq/generate-messages.d.ts +38 -0
- package/dist/mithaq/generate-messages.js +235 -0
- package/dist/mithaq/language.d.ts +374 -0
- package/dist/mithaq/language.js +76 -0
- package/dist/mithaq/lock.d.ts +71 -0
- package/dist/mithaq/lock.js +329 -0
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.d.ts +55 -0
- package/dist/mithaq/messages.js +111 -0
- package/dist/mithaq/refusal.d.ts +6 -0
- package/dist/mithaq/refusal.js +6 -0
- package/dist/mithaq/resolve.d.ts +10 -0
- package/dist/mithaq/resolve.js +1014 -0
- package/dist/mithaq/rows.d.ts +87 -0
- package/dist/mithaq/rows.js +174 -0
- package/dist/mithaq/rukn.d.ts +72 -0
- package/dist/mithaq/rukn.js +15 -0
- package/dist/mithaq/standard-schema.d.ts +38 -0
- package/dist/mithaq/standard-schema.js +2 -0
- package/dist/mithaq/types.d.ts +386 -0
- package/dist/mithaq/types.js +15 -0
- package/dist/mithaq/user.d.ts +19 -0
- package/dist/mithaq/user.js +16 -0
- package/dist/mithaq/where.d.ts +32 -0
- package/dist/mithaq/where.js +87 -0
- package/dist/rbac/index.d.ts +105 -0
- package/dist/rbac/index.js +182 -0
- package/dist/rbac/steps.d.ts +22 -0
- package/dist/rbac/steps.js +161 -0
- package/dist/rich-text/index.d.ts +28 -0
- package/dist/rich-text/index.js +59 -0
- package/dist/rich-text/rich-text.d.ts +106 -0
- package/dist/rich-text/rich-text.js +249 -0
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +156 -0
- package/dist/screen/client.js +71 -0
- package/dist/screen/generate.d.ts +14 -0
- package/dist/screen/generate.js +268 -0
- package/dist/screen/index.d.ts +496 -0
- package/dist/screen/index.js +663 -0
- package/dist/screen/react.d.ts +97 -0
- package/dist/screen/react.js +58 -0
- package/dist/screen/studio.d.ts +24 -0
- package/dist/screen/studio.js +68 -0
- package/dist/service/index.d.ts +63 -0
- package/dist/service/index.js +15 -0
- package/dist/storage/allowed-type.d.ts +5 -0
- package/dist/storage/allowed-type.js +7 -0
- package/dist/storage/index.d.ts +42 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/storage.d.ts +386 -0
- package/dist/storage/storage.js +412 -0
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/versions/index.d.ts +37 -0
- package/dist/versions/index.js +8 -0
- package/dist/versions/versions.d.ts +104 -0
- package/dist/versions/versions.js +160 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +193 -0
- package/dist/zod/index.js +122 -0
- package/dist/zod/kinds.d.ts +36 -0
- package/dist/zod/kinds.js +58 -0
- package/dist/zod/rules.d.ts +117 -0
- package/dist/zod/rules.js +67 -0
- 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 +92 -4
- 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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { type FieldList, type RowFieldWhere } from './rows.ts';
|
|
2
|
+
import type { Ref } from './user.ts';
|
|
3
|
+
import { defaultGlobalOperations, defaultOperations, type ContextOf, type DefaultLists, type OperationDeclaration, type SchemasOf } from '../asas/runner/steps.ts';
|
|
4
|
+
import { defaultFields, defaultGlobalFields } from '../base/fields.ts';
|
|
5
|
+
import { draftFields, versionOperations } from '../versions/versions.ts';
|
|
6
|
+
import type { KayanVersions } from '../versions/index.ts';
|
|
7
|
+
import type { DerivedInput, OperationParsed } from '../asas/runner/write.ts';
|
|
8
|
+
import type { Language } from './language.ts';
|
|
9
|
+
import type { ActorBoundAccess, Access, Field, FieldOf, Indexes, KayanService, KindValue, LocalesOf, MessageTree, RuntimeField, ServiceOf } from './types.ts';
|
|
10
|
+
import type { AuthStrategy } from '../asas/actor.ts';
|
|
11
|
+
import type { KayanConfigOf } from './rukn.ts';
|
|
12
|
+
type KayanAuthOf<Targets> = AuthStrategy | {
|
|
13
|
+
provider: 'local';
|
|
14
|
+
options: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
provider: 'better-auth';
|
|
17
|
+
server: (targets: Targets, vocabulary: {
|
|
18
|
+
catalog: readonly string[];
|
|
19
|
+
statements: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
referenced: (id: string) => Promise<number>;
|
|
21
|
+
}) => unknown;
|
|
22
|
+
};
|
|
23
|
+
type StrayLocales<Copy, Locale extends string> = {
|
|
24
|
+
[Stray in Exclude<keyof Copy, Locale>]: never;
|
|
25
|
+
};
|
|
26
|
+
type StrayMessages<Copy, Locale extends string> = Copy extends Readonly<Record<string, string>> ? StrayLocales<Copy, Locale> : {
|
|
27
|
+
[Key in keyof Copy]: StrayMessages<Copy[Key], Locale>;
|
|
28
|
+
};
|
|
29
|
+
type StrayDescription<F, Locale extends string> = F extends {
|
|
30
|
+
description: infer Copy;
|
|
31
|
+
} ? {
|
|
32
|
+
description: StrayLocales<Copy, Locale>;
|
|
33
|
+
} : unknown;
|
|
34
|
+
type StrayPlaceholder<F, Locale extends string> = F extends {
|
|
35
|
+
placeholder: infer Copy;
|
|
36
|
+
} ? {
|
|
37
|
+
placeholder: StrayLocales<Copy, Locale>;
|
|
38
|
+
} : unknown;
|
|
39
|
+
type KayanTargetsOf<L extends Language, Fields extends Record<string, unknown>, Service extends {
|
|
40
|
+
service?: unknown;
|
|
41
|
+
} = {
|
|
42
|
+
service?: KayanService;
|
|
43
|
+
}> = {
|
|
44
|
+
[Name in Exclude<keyof L['targets'], 'service'>]?: L['targets'][Name] extends {
|
|
45
|
+
capability: 'tenant';
|
|
46
|
+
} ? never : KayanConfigOf<L, Fields, L['targets'][Name]>;
|
|
47
|
+
} & Service;
|
|
48
|
+
/** The language's tenant target, which is under the key `tenant` alone, or `never`. */
|
|
49
|
+
type TenantOf<L extends Language> = L['targets'] extends {
|
|
50
|
+
tenant: infer T extends {
|
|
51
|
+
capability: 'tenant';
|
|
52
|
+
relationTo: string;
|
|
53
|
+
field: {
|
|
54
|
+
rules: object;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
} ? T : never;
|
|
58
|
+
type TenantBinding<L extends Language, F> = F extends {
|
|
59
|
+
targets: {
|
|
60
|
+
tenant: unknown;
|
|
61
|
+
};
|
|
62
|
+
} ? [TenantOf<L>] extends [never] ? {
|
|
63
|
+
targets: {
|
|
64
|
+
tenant: never;
|
|
65
|
+
};
|
|
66
|
+
} : {
|
|
67
|
+
kind: 'relationship';
|
|
68
|
+
relationTo: TenantOf<L>['relationTo'] | (() => {
|
|
69
|
+
name: TenantOf<L>['relationTo'];
|
|
70
|
+
});
|
|
71
|
+
hasMany?: never;
|
|
72
|
+
rules?: {
|
|
73
|
+
required?: true;
|
|
74
|
+
index?: true;
|
|
75
|
+
providedBy?: {
|
|
76
|
+
readonly create: 'application';
|
|
77
|
+
readonly update: 'never';
|
|
78
|
+
};
|
|
79
|
+
onDelete?: 'restrict';
|
|
80
|
+
defaultValue?: never;
|
|
81
|
+
};
|
|
82
|
+
targets: {
|
|
83
|
+
tenant: true;
|
|
84
|
+
} & {
|
|
85
|
+
[Name in StoreKeys<L['targets']> | SchemaKeys<L['targets']>]?: never;
|
|
86
|
+
};
|
|
87
|
+
} : unknown;
|
|
88
|
+
type TenantKey<Fields> = {
|
|
89
|
+
[N in keyof Fields]: Fields[N] extends {
|
|
90
|
+
targets: {
|
|
91
|
+
tenant: true;
|
|
92
|
+
};
|
|
93
|
+
} ? N : never;
|
|
94
|
+
}[keyof Fields] & string;
|
|
95
|
+
type TenantDefaulted<L extends Language, Fields> = true extends {
|
|
96
|
+
[N in keyof Fields]: Fields[N] extends {
|
|
97
|
+
targets: {
|
|
98
|
+
tenant: true;
|
|
99
|
+
};
|
|
100
|
+
} ? true : never;
|
|
101
|
+
}[keyof Fields] ? {
|
|
102
|
+
[N in keyof Fields]: Fields[N] extends {
|
|
103
|
+
targets: {
|
|
104
|
+
tenant: true;
|
|
105
|
+
};
|
|
106
|
+
} ? Fields[N] & {
|
|
107
|
+
rules: TenantOf<L>['field']['rules'];
|
|
108
|
+
} : Fields[N];
|
|
109
|
+
} : Fields;
|
|
110
|
+
type OperationOf<L extends Language, Fields extends Record<string, RuntimeField>, Data extends Record<string, unknown> = Record<string, unknown>> = [ServiceOf<L>] extends [never] ? never : OperationDeclaration<DefaultLists, SchemasOf<L, Fields>, LocalesOf<L>, FieldOf<L>, Data, ContextOf<ServiceOf<L>>>;
|
|
111
|
+
type DataOf<L extends Language, Fields extends Record<string, RuntimeField>, Operation> = OperationParsed<Operation, DerivedInput<Operation, Fields, L, []>> extends infer Data ? Data extends Record<string, unknown> ? Data : Record<string, unknown> : Record<string, unknown>;
|
|
112
|
+
type StoreKeys<Targets> = {
|
|
113
|
+
[Name in keyof Targets]: Targets[Name] extends {
|
|
114
|
+
capability: 'store';
|
|
115
|
+
} ? Name : never;
|
|
116
|
+
}[keyof Targets] & string;
|
|
117
|
+
type SchemaKeys<Targets> = {
|
|
118
|
+
[Name in keyof Targets]: Targets[Name] extends {
|
|
119
|
+
capability: 'screen' | 'service' | 'storage' | 'store' | 'tenant' | 'versions';
|
|
120
|
+
} ? never : Name;
|
|
121
|
+
}[keyof Targets] & string;
|
|
122
|
+
type NamedStore<Name> = [Name] extends [undefined] ? {
|
|
123
|
+
store?: undefined;
|
|
124
|
+
} : {
|
|
125
|
+
store: Name;
|
|
126
|
+
};
|
|
127
|
+
type NamedSchema<Name> = [Name] extends [undefined] ? {
|
|
128
|
+
schema?: undefined;
|
|
129
|
+
} : {
|
|
130
|
+
schema: Name;
|
|
131
|
+
};
|
|
132
|
+
type CheckedFields<L extends Language, Fields extends Record<string, FieldOf<L>>> = Fields & {
|
|
133
|
+
[N in keyof Fields]: {
|
|
134
|
+
[Key in Exclude<keyof Fields[N], keyof Field<string, L['rules']>>]: never;
|
|
135
|
+
} & StrayLocales<Fields[N]['label'], LocalesOf<L>> & StrayDescription<Fields[N], LocalesOf<L>> & StrayPlaceholder<Fields[N], LocalesOf<L>> & TenantBinding<L, Fields[N]>;
|
|
136
|
+
};
|
|
137
|
+
type AnyFunction = (...args: never) => unknown;
|
|
138
|
+
/**
|
|
139
|
+
* `operations` written as data `O`, or as a function of the defaults whose result is `R`. The two
|
|
140
|
+
* are told apart by `O` having no keys, never by a union: under a union TypeScript stops checking a
|
|
141
|
+
* written object for excess properties, and a misspelled key would pass. A stray key in a returned
|
|
142
|
+
* object, which no excess check reaches, is refused by name.
|
|
143
|
+
*/
|
|
144
|
+
type DeclaredOperations<L extends Language, Fields extends Record<string, RuntimeField>, O, R, Defaults> = O & ([keyof O] extends [never] ? ((args: {
|
|
145
|
+
defaults: Defaults;
|
|
146
|
+
}) => R & NoInfer<Record<string, OperationOf<L, Fields, never>> & {
|
|
147
|
+
[Name in keyof R]: OperationOf<L, Fields, DataOf<L, Fields, R[Name]>> & {
|
|
148
|
+
[Key in Exclude<keyof R[Name], keyof OperationOf<L, Fields, never>>]?: never;
|
|
149
|
+
};
|
|
150
|
+
}>) | NoInfer<Record<string, OperationOf<L, Fields, never>>> : NoInfer<Record<string, OperationOf<L, Fields, never>>> & NoInfer<{
|
|
151
|
+
[Name in keyof O]: OperationOf<L, Fields, DataOf<L, Fields, O[Name]>>;
|
|
152
|
+
}>);
|
|
153
|
+
/** The operations a kayan declared, whichever way it wrote them. */
|
|
154
|
+
type OperationsOf<O, R> = [keyof O] extends [never] ? ([O] extends [AnyFunction] ? R : O) : O;
|
|
155
|
+
/** Data, or a function of what the language offers that returns it, run once where the kayan is declared. */
|
|
156
|
+
type OverDefaults<Declared, Defaults> = Declared | ((args: {
|
|
157
|
+
defaults: Defaults;
|
|
158
|
+
}) => Declared);
|
|
159
|
+
/** The name of the language's versions target, which a kayan keeps versions through by writing an entry under it. */
|
|
160
|
+
type VersionsKey<L extends Language> = {
|
|
161
|
+
[Name in keyof L['targets']]: L['targets'][Name] extends {
|
|
162
|
+
capability: 'versions';
|
|
163
|
+
} ? Name : never;
|
|
164
|
+
}[keyof L['targets']];
|
|
165
|
+
/** What `fields: ({ defaults }) => ...` is handed: the id and timestamps, and the status field of a kayan that keeps drafts. */
|
|
166
|
+
type FieldDefaults<Kept> = typeof defaultFields & (Kept extends {
|
|
167
|
+
readonly drafts: true | object;
|
|
168
|
+
} ? typeof draftFields : unknown);
|
|
169
|
+
/** What `operations: ({ defaults }) => ...` is handed: the shipped operations, and the version ones of a kayan that keeps versions. */
|
|
170
|
+
type OperationDefaults<Kept> = typeof defaultOperations & ([Kept] extends [undefined] ? unknown : typeof versionOperations);
|
|
171
|
+
type Paths<Fields> = Extract<keyof Fields, string> | {
|
|
172
|
+
[Name in Extract<keyof Fields, string>]: Fields[Name] extends {
|
|
173
|
+
relationTo: () => {
|
|
174
|
+
fields: infer Target;
|
|
175
|
+
};
|
|
176
|
+
} ? `${Name}.${Extract<keyof Target, string>}` : Fields[Name] extends {
|
|
177
|
+
relationTo: string;
|
|
178
|
+
} ? `${Name}.${string}` : never;
|
|
179
|
+
}[Extract<keyof Fields, string>];
|
|
180
|
+
type FieldValue<F, L extends Language> = F extends {
|
|
181
|
+
kind: infer Name extends keyof L['kinds'];
|
|
182
|
+
} ? L['kinds'][Name] extends {
|
|
183
|
+
pointsAtRow: true;
|
|
184
|
+
} ? unknown : KindValue<L['kinds'][Name]> : unknown;
|
|
185
|
+
type CheckedOperand<Operator, Operand, Value> = Operand extends Ref ? Operand extends (Operator extends 'in' | 'not_in' ? Ref<readonly Value[]> : Ref<Value>) ? Operand : never : Operand;
|
|
186
|
+
/** Every key of a row's `Where` is a path of this kayan, or it is `never`. */
|
|
187
|
+
type CheckedWhere<W, Path extends string, Fields, L extends Language> = {
|
|
188
|
+
[Key in keyof W]: Key extends 'and' | 'or' ? W[Key] extends infer List extends readonly unknown[] ? {
|
|
189
|
+
[At in keyof List]: CheckedWhere<List[At], Path, Fields, L>;
|
|
190
|
+
} : never : Key extends Path ? W[Key] extends RowFieldWhere ? {
|
|
191
|
+
[Operator in keyof W[Key]]: CheckedOperand<Operator, W[Key][Operator], Key extends keyof Fields ? FieldValue<Fields[Key], L> : unknown>;
|
|
192
|
+
} : never : never;
|
|
193
|
+
};
|
|
194
|
+
type CheckedAnswer<Answer, Path extends string, Fields, L extends Language> = Answer extends boolean ? Answer : CheckedWhere<Answer, Path, Fields, L>;
|
|
195
|
+
type CheckedRow<Row, Path extends string, Name extends string, Fields, L extends Language> = {
|
|
196
|
+
[Key in keyof Row]: Key extends 'fields' ? Row[Key] extends readonly FieldList<Name>[] ? Row[Key] : never : Key extends 'permissions' ? Row[Key] extends '*' | readonly string[] ? Row[Key] : never : Key extends 'create' | 'delete' | 'read' | 'update' ? CheckedAnswer<Row[Key], Path, Fields, L> : never;
|
|
197
|
+
};
|
|
198
|
+
type CheckedRows<AccessDef, Fields, L extends Language> = AccessDef extends {
|
|
199
|
+
rows: infer Table;
|
|
200
|
+
} ? {
|
|
201
|
+
rows: {
|
|
202
|
+
[Role in keyof Table]: CheckedRow<Table[Role], Paths<Fields>, Extract<keyof Fields, string>, Fields, L>;
|
|
203
|
+
};
|
|
204
|
+
} : unknown;
|
|
205
|
+
/** Bound to one language, so a kayan is checked against that language's kinds. */
|
|
206
|
+
export declare const defineKayan: <const L extends Language>(language: L) => <const Name extends string, const Fields extends Record<string, FieldOf<L>>, const Messages extends MessageTree<LocalesOf<L>> = Record<never, never>, const O extends Record<string, OperationOf<L, Fields, never>> | AnyFunction = Record<never, never>, const AccessDef extends ({
|
|
207
|
+
create?: Access;
|
|
208
|
+
read?: Access;
|
|
209
|
+
update?: Access;
|
|
210
|
+
delete?: Access;
|
|
211
|
+
tenant?: false;
|
|
212
|
+
} | (ActorBoundAccess<unknown> & {
|
|
213
|
+
create?: unknown;
|
|
214
|
+
read?: unknown;
|
|
215
|
+
update?: unknown;
|
|
216
|
+
delete?: unknown;
|
|
217
|
+
})) = {
|
|
218
|
+
create?: Access;
|
|
219
|
+
read?: Access;
|
|
220
|
+
update?: Access;
|
|
221
|
+
delete?: Access;
|
|
222
|
+
}, const StoreName extends StoreKeys<L['targets']> | undefined = undefined, const SchemaName extends SchemaKeys<L['targets']> | undefined = undefined, const Auth extends KayanAuthOf<L['targets']> | undefined = undefined, const DbName extends string | undefined = undefined, const Kept extends KayanVersions | undefined = undefined, const R = Record<never, never>>(kayan: {
|
|
223
|
+
name: Name;
|
|
224
|
+
/** The provider that authenticates this kayan: `betterAuth({ server })`, `local({ secret })`, or a strategy. */
|
|
225
|
+
auth?: Auth;
|
|
226
|
+
/**
|
|
227
|
+
* The fields, in column order: `{ ...defaultFields, title }`, or `({ defaults }) => ({ ...defaults, title })`.
|
|
228
|
+
* TypeScript types a callback beside them that reads the fields, such as an operation's `input`
|
|
229
|
+
* over the derived shapes, before it reads what the function returns, so such a kayan writes them as an object.
|
|
230
|
+
*/
|
|
231
|
+
fields: OverDefaults<CheckedFields<L, Fields>, FieldDefaults<Kept>>;
|
|
232
|
+
/** This kayan's own copy, nested by the parts of each key. A key a field's own `label` also claims is a collision. */
|
|
233
|
+
messages?: Messages & { [Key in keyof Messages]: StrayMessages<Messages[Key], LocalesOf<L>>; };
|
|
234
|
+
/** Where `mithaq generate messages` writes this kayan's file, if not the default. */
|
|
235
|
+
out?: string;
|
|
236
|
+
/** What each target of this build says about this kayan, by the target's name: `{ drizzle: pgTable(...) }`, `targets.<screen>`, `targets.service`. */
|
|
237
|
+
targets?: KayanTargetsOf<L, Fields, {
|
|
238
|
+
service?: {
|
|
239
|
+
/** Every operation this kayan answers, and nothing else: `{ ...defaultOperations, archive }`, or `({ defaults }) => ({ ...defaults, archive })`. */
|
|
240
|
+
operations?: DeclaredOperations<L, Fields, O, R, OperationDefaults<Kept>>;
|
|
241
|
+
/** The store this kayan is written through. One of the language's targets. */
|
|
242
|
+
store?: StoreName;
|
|
243
|
+
/** The target a write is validated against. One of the language's targets. */
|
|
244
|
+
schema?: SchemaName;
|
|
245
|
+
};
|
|
246
|
+
}> & { [Name_1 in VersionsKey<L>]?: Kept; };
|
|
247
|
+
/** This kayan's own open bag, for the project's own code. Nothing in mithaq reads it, and it stays on the server. */
|
|
248
|
+
custom?: Record<string, unknown>;
|
|
249
|
+
access?: AccessDef & CheckedRows<AccessDef, Fields, L>;
|
|
250
|
+
/** The table name. Defaults to the name. */
|
|
251
|
+
dbName?: DbName;
|
|
252
|
+
/** The Postgres schema this kayan's tables are built in. No key, or `'public'`, is `public`. */
|
|
253
|
+
schemaName?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Indexes over this kayan's own columns, each over the fields it names, in order, and unique where it says so.
|
|
256
|
+
* A list is built beside the defaults; `({ defaults }) => [...]` answers the whole list. A group,
|
|
257
|
+
* `{ within: 'organization', unique: ['code'] }`, is indexes led by the tenant field.
|
|
258
|
+
*/
|
|
259
|
+
indexes?: Indexes<NoInfer<keyof Fields & string>, NoInfer<TenantKey<Fields>>>;
|
|
260
|
+
}) => {
|
|
261
|
+
name: Name;
|
|
262
|
+
auth?: Auth;
|
|
263
|
+
access?: AccessDef;
|
|
264
|
+
dbName?: DbName;
|
|
265
|
+
schemaName?: string;
|
|
266
|
+
indexes?: Indexes<keyof Fields & string, TenantKey<Fields>>;
|
|
267
|
+
messages?: Messages;
|
|
268
|
+
out?: string;
|
|
269
|
+
targets: KayanTargetsOf<L, Fields, {
|
|
270
|
+
service: {
|
|
271
|
+
operations: NoInfer<OperationsOf<O, R>>;
|
|
272
|
+
} & NamedStore<StoreName> & NamedSchema<SchemaName>;
|
|
273
|
+
}>;
|
|
274
|
+
custom?: Record<string, unknown>;
|
|
275
|
+
fields: TenantDefaulted<L, Fields>;
|
|
276
|
+
/** The language this kayan was declared in, on the type alone, so `tools.of(kayan)` types a call from it. */
|
|
277
|
+
readonly '~language'?: L;
|
|
278
|
+
};
|
|
279
|
+
/** The language a kayan's `'~language'` brand names, and the open language on one that carries none. */
|
|
280
|
+
export type LanguageOf<K> = K extends {
|
|
281
|
+
readonly '~language'?: infer L extends Language;
|
|
282
|
+
} ? L : Language;
|
|
283
|
+
/** A global's access: read and update, and no more. */
|
|
284
|
+
type GlobalAccess = {
|
|
285
|
+
read?: Access;
|
|
286
|
+
update?: Access;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Declares a global, a kayan of one row. It spreads `defaultGlobalFields` and runs
|
|
290
|
+
* `defaultGlobalOperations`; a tenant field makes it one row per tenant.
|
|
291
|
+
*/
|
|
292
|
+
export declare const defineGlobal: <const L extends Language>(language: L) => <const Name extends string, const Fields extends Record<string, FieldOf<L>>, const Messages extends MessageTree<LocalesOf<L>> = Record<never, never>, const O extends Record<string, OperationOf<L, Fields, never>> | AnyFunction = Record<never, never>, const AccessDef extends GlobalAccess = Record<never, never>, const StoreName extends StoreKeys<L['targets']> | undefined = undefined, const SchemaName extends SchemaKeys<L['targets']> | undefined = undefined, const DbName extends string | undefined = undefined, const R = Record<never, never>>(global: {
|
|
293
|
+
name: Name;
|
|
294
|
+
/** The fields, in column order: `{ ...defaultFields, ...defaultGlobalFields, siteTitle }`, or `({ defaults }) => ({ ...defaults, siteTitle })`. */
|
|
295
|
+
fields: OverDefaults<CheckedFields<L, Fields>, typeof defaultFields & typeof defaultGlobalFields>;
|
|
296
|
+
/** This global's own copy, nested by the parts of each key, as on any kayan. */
|
|
297
|
+
messages?: Messages & { [Key in keyof Messages]: StrayMessages<Messages[Key], LocalesOf<L>>; };
|
|
298
|
+
/** Where `mithaq generate messages` writes this global's file, if not the default. */
|
|
299
|
+
out?: string;
|
|
300
|
+
/** What each target says about this global, exactly as on any kayan. */
|
|
301
|
+
targets?: KayanTargetsOf<L, Fields, {
|
|
302
|
+
service?: {
|
|
303
|
+
/** The two operations a global runs, `defaultGlobalOperations` or a list of the same names, or `({ defaults }) => defaults`. */
|
|
304
|
+
operations?: DeclaredOperations<L, Fields, O, R, typeof defaultGlobalOperations>;
|
|
305
|
+
/** The store this global is written through. One of the language's targets. */
|
|
306
|
+
store?: StoreName;
|
|
307
|
+
/** The target a write is validated against. One of the language's targets. */
|
|
308
|
+
schema?: SchemaName;
|
|
309
|
+
};
|
|
310
|
+
}>;
|
|
311
|
+
/** This global's own open bag, for the project's own code. Nothing in mithaq reads it, and it stays on the server. */
|
|
312
|
+
custom?: Record<string, unknown>;
|
|
313
|
+
/** Read and update only: a create or delete key is refused by the type. */
|
|
314
|
+
access?: AccessDef & { [Key in Exclude<keyof AccessDef, 'read' | 'update'>]: never; };
|
|
315
|
+
/** The table name. Defaults to the name. */
|
|
316
|
+
dbName?: DbName;
|
|
317
|
+
/** The Postgres schema this global's tables are built in. No key, or `'public'`, is `public`. */
|
|
318
|
+
schemaName?: string;
|
|
319
|
+
}) => {
|
|
320
|
+
name: Name;
|
|
321
|
+
access?: AccessDef;
|
|
322
|
+
dbName?: DbName;
|
|
323
|
+
schemaName?: string;
|
|
324
|
+
single: true;
|
|
325
|
+
messages?: Messages;
|
|
326
|
+
out?: string;
|
|
327
|
+
targets: KayanTargetsOf<L, Fields, {
|
|
328
|
+
service: {
|
|
329
|
+
operations: NoInfer<OperationsOf<O, R>>;
|
|
330
|
+
} & NamedStore<StoreName> & NamedSchema<SchemaName>;
|
|
331
|
+
}>;
|
|
332
|
+
custom?: Record<string, unknown>;
|
|
333
|
+
fields: TenantDefaulted<L, Fields>;
|
|
334
|
+
};
|
|
335
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { applyRows, rowFields, rowsOf } from './rows.js';
|
|
2
|
+
import { isVersions, languageRows, serviceTargetOf, tenantDefaults } from './language.js';
|
|
3
|
+
import { defaultGlobalOperations, defaultOperations, resolveOperations } from '../asas/runner/steps.js';
|
|
4
|
+
import { defaultFields, defaultGlobalFields } from '../base/fields.js';
|
|
5
|
+
import { draftFields, versionOperations } from '../versions/versions.js';
|
|
6
|
+
const overDefaults = (declared, defaults) => typeof declared === 'function' ? declared({ defaults }) : declared;
|
|
7
|
+
/** Bound to one language, so a kayan is checked against that language's kinds. */
|
|
8
|
+
export const defineKayan = (language) => (kayan) => {
|
|
9
|
+
const versioning = Object.keys(language.targets).find((name) => isVersions(language.targets[name]));
|
|
10
|
+
const kept = versioning === undefined ? undefined : kayan.targets?.[versioning];
|
|
11
|
+
const declaredFields = overDefaults(kayan.fields, { ...defaultFields, ...(kept?.drafts && draftFields) });
|
|
12
|
+
// The key order is the column order but for the timestamps, which a new table puts last: nothing is merged in.
|
|
13
|
+
const fields = Object.fromEntries(Object.entries(declaredFields).map(([name, field]) => [name, tenantDefaults(language, field)]));
|
|
14
|
+
// A tenant-owned kayan inherits the language's rows; one opts out on its access.
|
|
15
|
+
const declared = kayan.access;
|
|
16
|
+
const owned = Object.values(fields).some((field) => field.targets?.tenant !== undefined);
|
|
17
|
+
const inherited = declared?.tenant === false || !owned ? undefined : languageRows(language);
|
|
18
|
+
const compiled = rowsOf(declared) ?? inherited;
|
|
19
|
+
if (compiled) {
|
|
20
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
21
|
+
// ANDed with the row's answer, so it narrows and never widens. The kayan's name goes
|
|
22
|
+
// with it, since one compiled table is shared by every kayan that inherits it.
|
|
23
|
+
fields[name] = {
|
|
24
|
+
...field,
|
|
25
|
+
access: rowFields({ compiled, name, declared: field.access, kayan: kayan.name }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// A steps function runs once, here, so the kayan carries data a tool reads without calling anything.
|
|
30
|
+
const own = kayan.targets?.service;
|
|
31
|
+
const declaredOperations = overDefaults(own?.operations, { ...defaultOperations, ...(kept && versionOperations) });
|
|
32
|
+
const operations = resolveOperations(declaredOperations, serviceTargetOf(language));
|
|
33
|
+
return {
|
|
34
|
+
...kayan,
|
|
35
|
+
...(compiled ? { access: applyRows(compiled, declared) } : {}),
|
|
36
|
+
fields,
|
|
37
|
+
targets: { ...kayan.targets, service: { ...own, operations } },
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Declares a global, a kayan of one row. It spreads `defaultGlobalFields` and runs
|
|
42
|
+
* `defaultGlobalOperations`; a tenant field makes it one row per tenant.
|
|
43
|
+
*/
|
|
44
|
+
export const defineGlobal = (language) => {
|
|
45
|
+
const kayan = defineKayan(language);
|
|
46
|
+
return (global) => {
|
|
47
|
+
const service = global.targets?.service;
|
|
48
|
+
const declared = {
|
|
49
|
+
...global,
|
|
50
|
+
fields: overDefaults(global.fields, { ...defaultFields, ...defaultGlobalFields }),
|
|
51
|
+
targets: { ...global.targets, service: { ...service, operations: overDefaults(service?.operations, defaultGlobalOperations) } },
|
|
52
|
+
};
|
|
53
|
+
// `defineKayan`'s own access object, untouched: a rebuilt one would drop the rows
|
|
54
|
+
// marker, and the permission check would stand aside for every global operation.
|
|
55
|
+
return ({ ...kayan(declared), single: true });
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MessageRegistry } from './messages.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What a run found: `changed`, the files a run writes, and the refusals, each naming what is wrong
|
|
4
|
+
* and where. `unverifiable`, the labels written as functions, is reported and never fails.
|
|
5
|
+
*/
|
|
6
|
+
export type MessagesReport = {
|
|
7
|
+
changed: string[];
|
|
8
|
+
missing: string[];
|
|
9
|
+
noSource: string[];
|
|
10
|
+
resolver: string[];
|
|
11
|
+
collisions: string[];
|
|
12
|
+
unverifiable: string[];
|
|
13
|
+
data: string[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Writes one file per scope, and with `check` writes nothing and reports what a run would change and every gap:
|
|
17
|
+
* a missing locale, a key with no source or missing from the resolver, a key two scopes claim, and rows short of a locale.
|
|
18
|
+
*/
|
|
19
|
+
export declare const generateMessages: ({ registry, directory, check, root, rows, }: {
|
|
20
|
+
registry: MessageRegistry;
|
|
21
|
+
directory: URL;
|
|
22
|
+
check?: boolean;
|
|
23
|
+
/** The project's own root, which a scope's own `out` is resolved from. */
|
|
24
|
+
root?: URL;
|
|
25
|
+
/** Reads a page of one kayan's rows, for the data gap. Omitted, the scan is skipped. */
|
|
26
|
+
rows?: (entry: MessageRegistry['localized'][number]) => Promise<Record<string, unknown>[]>;
|
|
27
|
+
}) => Promise<MessagesReport>;
|
|
28
|
+
/** Everything a `--check` refuses, which is everything but the unverifiable list. */
|
|
29
|
+
export declare const refusals: (report: MessagesReport) => string[];
|
|
30
|
+
/**
|
|
31
|
+
* `mithaq generate messages`, from the project's root, answering its exit code. The data scan connects
|
|
32
|
+
* read-only, and a check that could not scan fails: rows short of a locale break every read of the kayan.
|
|
33
|
+
*/
|
|
34
|
+
export declare const run: ({ project, check, dataScan }: {
|
|
35
|
+
project: URL;
|
|
36
|
+
check: boolean;
|
|
37
|
+
dataScan?: boolean;
|
|
38
|
+
}) => Promise<number>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { Refusal } from './refusal.js';
|
|
11
|
+
import { flattenMessageFile, nestMessageFile } from './message-file.js';
|
|
12
|
+
const fileOf = (name, out, directory, root) => out ? new URL(out, root) : new URL(`${name}.json`, directory);
|
|
13
|
+
// What is on disk wins: these files are the project's to translate, so a run adds a
|
|
14
|
+
// missing key and keeps every translation when rewriting the file shape.
|
|
15
|
+
const merge = (copy, current, locales) => {
|
|
16
|
+
if (typeof copy === 'function')
|
|
17
|
+
return null;
|
|
18
|
+
if (typeof copy === 'string')
|
|
19
|
+
return copy;
|
|
20
|
+
const held = current && typeof current === 'object' ? current : {};
|
|
21
|
+
// `??`, not `||`: a locale the project left empty is its answer, and is still reported missing.
|
|
22
|
+
return Object.fromEntries(locales.map((locale) => [locale, held[locale] ?? copy[locale] ?? '']));
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Writes one file per scope, and with `check` writes nothing and reports what a run would change and every gap:
|
|
26
|
+
* a missing locale, a key with no source or missing from the resolver, a key two scopes claim, and rows short of a locale.
|
|
27
|
+
*/
|
|
28
|
+
export const generateMessages = async ({ registry, directory, check = false, root = directory, rows, }) => {
|
|
29
|
+
// With no locale every record would be `{}` and every gate would pass, dropping
|
|
30
|
+
// every string and reporting nothing.
|
|
31
|
+
if (!registry.locales.length) {
|
|
32
|
+
throw new Refusal('The language declares no locales, so `mithaq generate messages` has nothing to write a copy file against.');
|
|
33
|
+
}
|
|
34
|
+
// Two scopes resolving to one path would each report the other's keys as having no source.
|
|
35
|
+
const files = registry.scopes.map((scope) => fileOf(scope.name, scope.out, directory, root));
|
|
36
|
+
files.forEach((file, at) => {
|
|
37
|
+
// Compared ignoring case, because macOS's default volume puts `courses.json` into
|
|
38
|
+
// an existing `Courses.json`.
|
|
39
|
+
const first = files.findIndex((one) => one.href.toLowerCase() === file.href.toLowerCase());
|
|
40
|
+
if (first !== at) {
|
|
41
|
+
throw new Refusal(`Scopes "${registry.scopes[first].name}" and "${registry.scopes[at].name}" both write ${file.pathname}. Give one of them its own \`out\`.`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// Read before any write, so a file that stops the run stops it before the first write.
|
|
45
|
+
const committed = [];
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
// A file that is there and does not parse is refused, not read as `{}`: every
|
|
48
|
+
// translation in it would be rewritten with nothing reported.
|
|
49
|
+
const held = await readFile(file, 'utf8').catch((error) => {
|
|
50
|
+
if (error.code === 'ENOENT')
|
|
51
|
+
return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
let parsed = {};
|
|
55
|
+
try {
|
|
56
|
+
if (held !== undefined)
|
|
57
|
+
parsed = JSON.parse(held);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
parsed = undefined;
|
|
61
|
+
}
|
|
62
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
63
|
+
throw new Refusal(`${file.pathname} is not a copy file \`mithaq generate messages\` can read, and generating would overwrite it. Fix it or delete it.`);
|
|
64
|
+
}
|
|
65
|
+
committed.push(parsed);
|
|
66
|
+
}
|
|
67
|
+
const report = { changed: [], missing: [], noSource: [], resolver: [], collisions: [], unverifiable: [], data: [] };
|
|
68
|
+
for (const [key, claims] of Object.entries(registry.collisions)) {
|
|
69
|
+
report.collisions.push(`"${key}" is claimed by ${claims.join(' and ')}.`);
|
|
70
|
+
}
|
|
71
|
+
// Needs no database, so it is found here and not hidden by `--no-data-scan`.
|
|
72
|
+
for (const kayan of new Set(registry.localized.filter((entry) => !entry.sort).map((entry) => entry.kayan))) {
|
|
73
|
+
report.data.push(`Kayan "${kayan}" has no field the store can sort a page by, so its localized fields cannot be scanned. Give it one field that is not localized.`);
|
|
74
|
+
}
|
|
75
|
+
// `messagesOf` puts this scope first, so it is filled before any kayan's scope reads it.
|
|
76
|
+
let floor = {};
|
|
77
|
+
for (const [at, scope] of registry.scopes.entries()) {
|
|
78
|
+
const file = files[at];
|
|
79
|
+
const current = flattenMessageFile({ file: committed[at], locales: registry.locales });
|
|
80
|
+
const wanted = { ...current };
|
|
81
|
+
for (const key of Object.keys(current)) {
|
|
82
|
+
if (!Object.hasOwn(scope.keys, key))
|
|
83
|
+
report.noSource.push(`${scope.name}.${key} is an entry with no source. Delete it; a run keeps it.`);
|
|
84
|
+
}
|
|
85
|
+
if (scope.name === 'mithaq')
|
|
86
|
+
floor = wanted;
|
|
87
|
+
for (const key of Object.keys(scope.keys).sort()) {
|
|
88
|
+
const source = scope.seeds?.[key];
|
|
89
|
+
const from = source ? floor[source] : undefined;
|
|
90
|
+
let held = current[key];
|
|
91
|
+
// A locale still holding core's sentence is a seed rather than a person's, so a default
|
|
92
|
+
// changed in `mithaq.json` reaches every field that never said otherwise.
|
|
93
|
+
if (from && typeof from === 'object' && held && typeof held === 'object') {
|
|
94
|
+
const shipped = scope.keys[key];
|
|
95
|
+
held = Object.fromEntries(Object.entries(held).filter(([locale, value]) => value !== (shipped[locale] ?? '')));
|
|
96
|
+
}
|
|
97
|
+
const entry = merge(from && typeof from === 'object' ? from : scope.keys[key], held, registry.locales);
|
|
98
|
+
wanted[key] = entry;
|
|
99
|
+
if (entry === null) {
|
|
100
|
+
report.unverifiable.push(`${scope.name}.${key} is a function, so no locale of it can be verified.`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeof entry === 'string') {
|
|
104
|
+
report.unverifiable.push(`${scope.name}.${key} is one string the resolver returned, so no locale of it can be verified.`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const locale of registry.locales) {
|
|
108
|
+
if (!entry[locale])
|
|
109
|
+
report.missing.push(`${scope.name}.${key} has no "${locale}".`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const source = `${JSON.stringify(nestMessageFile(wanted), null, 2)}\n`;
|
|
113
|
+
if (source === `${JSON.stringify(committed[at], null, 2)}\n`)
|
|
114
|
+
continue;
|
|
115
|
+
report.changed.push(scope.out ?? `${scope.name}.json`);
|
|
116
|
+
if (check)
|
|
117
|
+
continue;
|
|
118
|
+
// Per file: a scope's own `out` may name a directory no run has made yet.
|
|
119
|
+
await mkdir(new URL('.', file), { recursive: true });
|
|
120
|
+
await writeFile(file, source);
|
|
121
|
+
}
|
|
122
|
+
// The same refusal as the type error, for a build where a resolver reached this far short of a key.
|
|
123
|
+
if (registry.resolved) {
|
|
124
|
+
const shipped = registry.scopes.find((scope) => scope.name === 'mithaq');
|
|
125
|
+
const project = registry.scopes.find((scope) => scope.name === 'project');
|
|
126
|
+
for (const key of Object.keys(shipped.keys)) {
|
|
127
|
+
// A refusal sentence seeds every per-field key one locale at a time, which one opaque string cannot.
|
|
128
|
+
if (/^(rules\.|kinds\.[^.]+\.invalid$)/.test(key))
|
|
129
|
+
continue;
|
|
130
|
+
if (!(key in project.keys))
|
|
131
|
+
report.resolver.push(`The messages resolver returned no "${key}".`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (rows) {
|
|
135
|
+
for (const entry of registry.localized) {
|
|
136
|
+
// Already named above, once for the kayan rather than once for each of
|
|
137
|
+
// its localized fields, and there is no page to ask for without a sort.
|
|
138
|
+
if (!entry.sort)
|
|
139
|
+
continue;
|
|
140
|
+
for (const row of await rows(entry)) {
|
|
141
|
+
const value = row[entry.field];
|
|
142
|
+
if (!value || typeof value !== 'object')
|
|
143
|
+
continue;
|
|
144
|
+
const held = Object.values(value);
|
|
145
|
+
if (held.some((one) => one === null) && held.some((one) => one !== null)) {
|
|
146
|
+
report.data.push(`Kayan "${entry.kayan}" field "${entry.field}" holds a row written before a locale was added, so every read of that kayan fails. Backfill it.`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return report;
|
|
153
|
+
};
|
|
154
|
+
/** Everything a `--check` refuses, which is everything but the unverifiable list. */
|
|
155
|
+
export const refusals = (report) => [
|
|
156
|
+
...report.collisions,
|
|
157
|
+
...report.resolver,
|
|
158
|
+
...report.missing,
|
|
159
|
+
...report.noSource,
|
|
160
|
+
...report.data,
|
|
161
|
+
...report.changed.map((file) => `${file} is not what a run would write.`),
|
|
162
|
+
];
|
|
163
|
+
/**
|
|
164
|
+
* `mithaq generate messages`, from the project's root, answering its exit code. The data scan connects
|
|
165
|
+
* read-only, and a check that could not scan fails: rows short of a locale break every read of the kayan.
|
|
166
|
+
*/
|
|
167
|
+
export const run = async ({ project, check, dataScan = true }) => {
|
|
168
|
+
const { generate } = (await import(__rewriteRelativeImportExtension(new URL('src/mithaq.ts', project).href)));
|
|
169
|
+
const directory = new URL(generate?.messages ?? 'src/messages/', project);
|
|
170
|
+
const { asas } = (await import(__rewriteRelativeImportExtension(new URL('src/asas.ts', project).href)));
|
|
171
|
+
let rows;
|
|
172
|
+
/** Why the scan did not answer, which fails the check rather than shading it. */
|
|
173
|
+
let unscanned = '';
|
|
174
|
+
if (check && dataScan && asas.messages.localized.length) {
|
|
175
|
+
try {
|
|
176
|
+
// Read-only: a connect that pushed would apply a migration past its guards and seed the roles.
|
|
177
|
+
await asas.connect({ readOnly: true });
|
|
178
|
+
rows = async (entry) => {
|
|
179
|
+
try {
|
|
180
|
+
return (await asas.targets[entry.store].find({
|
|
181
|
+
kayan: entry.kayan,
|
|
182
|
+
args: { limit: 0, page: 1, sort: { field: entry.sort, direction: 'asc' } },
|
|
183
|
+
})).docs;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// The scan did not run, so the check cannot say the table is clean.
|
|
187
|
+
unscanned = error.message;
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
unscanned = error.message;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// In a `finally`: a refusal is a throw, and one escaping it would leave the connection open.
|
|
197
|
+
let report;
|
|
198
|
+
try {
|
|
199
|
+
report = await generateMessages({ registry: asas.messages, directory, check, root: project, rows });
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
// `try`/`catch`: a synchronous throw from `destroy` returns nothing to attach a `.catch()` to.
|
|
203
|
+
try {
|
|
204
|
+
await asas.destroy();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Nothing was connected, which is the ordinary path.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const line of report.unverifiable)
|
|
211
|
+
console.log(`Unverifiable: ${line}`);
|
|
212
|
+
if (check && !dataScan && asas.messages.localized.length) {
|
|
213
|
+
console.log('The data scan was skipped: --no-data-scan.');
|
|
214
|
+
}
|
|
215
|
+
const refused = refusals(report);
|
|
216
|
+
if (!check)
|
|
217
|
+
for (const line of report.noSource)
|
|
218
|
+
console.log(line);
|
|
219
|
+
if (check && (refused.length || unscanned)) {
|
|
220
|
+
for (const line of refused)
|
|
221
|
+
console.error(line);
|
|
222
|
+
// Only a file a run would rewrite: a run leaves an empty locale, a
|
|
223
|
+
// collision, a resolver gap and a data gap exactly as they are.
|
|
224
|
+
if (report.changed.length)
|
|
225
|
+
console.error('Run `mithaq generate messages`.');
|
|
226
|
+
// Its own line: it is no gap in the copy, but the one a diff cannot show went unmeasured.
|
|
227
|
+
if (unscanned) {
|
|
228
|
+
console.error(`The data scan did not run: ${unscanned}`);
|
|
229
|
+
console.error('Run it against the database, or pass `--no-data-scan` on a project that has none.');
|
|
230
|
+
}
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
console.log(report.changed.length ? `Wrote ${report.changed.sort().join(', ')}.` : 'Messages are up to date.');
|
|
234
|
+
return 0;
|
|
235
|
+
};
|