@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,358 @@
1
+ import type { KayanAuth } from '../asas/actor.ts';
2
+ import type { Ref } from './user.ts';
3
+ import type { AnyOperation, ResolvedOperation } from '../asas/runner/steps.ts';
4
+ import type { Operator, Where } from './where.ts';
5
+ import type { StandardSchemaV1 } from './standard-schema.ts';
6
+ import type { Rules } from '../base/rules.ts';
7
+ import type { Rule, RuleValue } from '../base/rules.ts';
8
+ import type { Kind } from '../base/kinds.ts';
9
+ import type { RichTextRead } from '../rich-text/rich-text.ts';
10
+ import type { ResolvedStorage } from '../storage/storage.ts';
11
+ import type { RefOf, ScreenField, ScreenKeys } from '../screen/index.ts';
12
+ /** What a field access callback is handed. */
13
+ export type FieldAccessArgs<User = unknown, Doc extends Record<string, unknown> = Record<string, unknown>, Id = unknown, Data extends Record<string, unknown> = Partial<Doc>> = {
14
+ data?: Data;
15
+ doc?: Doc;
16
+ id?: Id;
17
+ req?: unknown;
18
+ siblingData?: Record<string, unknown>;
19
+ user: User | null;
20
+ };
21
+ export type FieldAccess<User = unknown, Doc extends Record<string, unknown> = Record<string, unknown>, Id = unknown, Data extends Record<string, unknown> = Partial<Doc>> = (args: FieldAccessArgs<User, Doc, Id, Data>) => boolean | Promise<boolean>;
22
+ type StoredFieldAccess = FieldAccess | (ActorBoundAccess<unknown> & {
23
+ readonly __mithaqFieldData: unknown;
24
+ readonly __mithaqFieldId: unknown;
25
+ });
26
+ type StoredAccess = {
27
+ create?: Access;
28
+ read?: Access;
29
+ update?: Access;
30
+ delete?: Access;
31
+ } | (ActorBoundAccess<unknown> & {
32
+ create?: unknown;
33
+ read?: unknown;
34
+ update?: unknown;
35
+ delete?: unknown;
36
+ });
37
+ /** Copy in every locale a project declares, one string each. */
38
+ export type Localized<Locale extends string = string> = Readonly<Record<Locale, string>>;
39
+ /** A label a project resolves itself, such as a compiled paraglide message: `label: m.course_title`. It takes nothing. */
40
+ export type LabelFunction = () => string;
41
+ /** A scope's own copy, keyed flat: one entry per key, each a record per locale or a function. */
42
+ export type MessageMap<Locale extends string = string> = Readonly<Record<string, Localized<Locale> | LabelFunction>>;
43
+ /** A scope's copy as a declaration writes it, nested by the parts of the key, so a field's name is written once. */
44
+ export interface MessageTree<Locale extends string = string> {
45
+ readonly [key: string]: Localized<Locale> | LabelFunction | MessageTree<Locale>;
46
+ }
47
+ /** The locales a language declared, or any string when it declared none. */
48
+ export type LocalesOf<L> = L extends {
49
+ locales: readonly (infer Locale extends string)[];
50
+ } ? Locale : string;
51
+ /** The service target a language declares, found by its capability. */
52
+ export type ServiceOf<L> = L extends {
53
+ targets: infer Targets;
54
+ } ? Extract<Targets[keyof Targets], {
55
+ capability: 'service';
56
+ }> : never;
57
+ type Named<Locale extends string = string> = {
58
+ access?: {
59
+ create?: StoredFieldAccess;
60
+ read?: StoredFieldAccess;
61
+ update?: StoredFieldAccess;
62
+ };
63
+ /** One string per locale the language declares, every one required. */
64
+ label?: Localized<Locale> | LabelFunction;
65
+ /** What the field is, in a sentence, and the hint its empty control shows. */
66
+ description?: Localized<Locale> | LabelFunction;
67
+ placeholder?: Localized<Locale> | LabelFunction;
68
+ /** What a text or number field's input group draws before and after its value. */
69
+ prefix?: Localized<Locale>;
70
+ suffix?: Localized<Locale>;
71
+ /** The closed list a text field's value is one of. */
72
+ options?: readonly {
73
+ readonly value: string;
74
+ readonly label: Localized<Locale>;
75
+ }[];
76
+ /** The column name. A relationship without one gets `<snake_case name>_id`. */
77
+ dbName?: string;
78
+ /**
79
+ * One entry per target that has something to say about this field, by the target's name. A store's or
80
+ * a schema target's entry is the whole built thing, used as-is with the rules skipped; a screen's says how it draws the field.
81
+ */
82
+ targets?: Record<string, unknown>;
83
+ /** An open bag the project's own code reads; nothing in mithaq does, and it stays on the server. */
84
+ custom?: Record<string, unknown>;
85
+ };
86
+ type FieldKind<KindName extends string> = KindName extends 'join' ? {
87
+ kind: KindName;
88
+ /** The kayan whose rows this field holds, as a thunk or a name. */
89
+ relationTo: string | (() => Kayan);
90
+ /** The child's own relationship field that holds this row's id. */
91
+ on: string;
92
+ /** How many of the child's rows one populated read carries, over the service target's `joinLimit`. */
93
+ defaultLimit?: number;
94
+ /** The order the child's rows come back in: one field of the child, `-` for descending. */
95
+ defaultSort?: string;
96
+ hasMany?: never;
97
+ } : KindName extends 'relationship' ? {
98
+ kind: KindName;
99
+ /** The kayan this field points at, as a thunk, so two kayan may point at each other, or as a name. */
100
+ relationTo: string | (() => Kayan);
101
+ /** Whether this field holds a set of rows rather than one, kept in a join table and read back as an array of ids. */
102
+ hasMany?: true;
103
+ on?: never;
104
+ defaultLimit?: never;
105
+ defaultSort?: never;
106
+ } : {
107
+ kind: KindName;
108
+ relationTo?: never;
109
+ hasMany?: never;
110
+ on?: never;
111
+ defaultLimit?: never;
112
+ defaultSort?: never;
113
+ };
114
+ /** A field, open over locales: a project's own declaration goes through `FieldOf<L>`, which narrows them. */
115
+ export type Field<KindName extends string = string, RuleValues = Rules> = KindName extends unknown ? Named & {
116
+ rules?: Partial<RuleValues>;
117
+ } & FieldKind<KindName> : never;
118
+ /** A field as it is after a build, open over locales for `Field`'s reason. */
119
+ export type RuntimeField = Named & {
120
+ kind: string;
121
+ rules?: Record<string, unknown>;
122
+ relationTo?: string | (() => Kayan);
123
+ hasMany?: true;
124
+ /** A `join` field only: the child's relationship that points back at this kayan. */
125
+ on?: string;
126
+ /** A `join` field only: how many of the child's rows one populated read carries. */
127
+ defaultLimit?: number;
128
+ /** A `join` field only: the order the child's rows come back in. */
129
+ defaultSort?: string;
130
+ };
131
+ export type LanguageShape = {
132
+ kinds: Record<string, Kind>;
133
+ rules: Record<string, Rule>;
134
+ targets: Record<string, unknown>;
135
+ locales?: readonly string[];
136
+ };
137
+ type RuleNames<K> = K extends Kind<unknown, infer Name> ? Name : never;
138
+ export type KindValue<K> = K extends Kind<infer Value, string> ? Value : never;
139
+ type BoundRules<L extends LanguageShape, K extends keyof L['kinds']> = {
140
+ [Name in Extract<RuleNames<L['kinds'][K]>, keyof L['rules']>]?: RuleValue<L['rules'][Name]>;
141
+ } & ('defaultValue' extends RuleNames<L['kinds'][K]> ? {
142
+ defaultValue?: KindValue<L['kinds'][K]> | Ref<KindValue<L['kinds'][K]>>;
143
+ } : {});
144
+ type BoundField<L extends LanguageShape, K extends keyof L['kinds'] & string> = Named<LocalesOf<L>> & {
145
+ kind: K;
146
+ rules?: BoundRules<L, K>;
147
+ targets?: Record<string, unknown> & {
148
+ [Screen in ScreenKeys<L['targets']>]?: ScreenField<RefOf<L['targets'][Screen], L['kinds']>>;
149
+ };
150
+ } & (K extends 'join' ? {
151
+ relationTo: string | (() => Kayan);
152
+ on: string;
153
+ defaultLimit?: number;
154
+ defaultSort?: string;
155
+ hasMany?: never;
156
+ } : L['kinds'][K] extends {
157
+ pointsAtRow: true;
158
+ } ? {
159
+ relationTo: string | (() => Kayan);
160
+ hasMany?: true;
161
+ on?: never;
162
+ defaultLimit?: never;
163
+ defaultSort?: never;
164
+ } : {
165
+ relationTo?: never;
166
+ hasMany?: never;
167
+ on?: never;
168
+ defaultLimit?: never;
169
+ defaultSort?: never;
170
+ });
171
+ /** Field union derived once from a concrete language's kind and rule registries. */
172
+ export type FieldOf<L extends LanguageShape> = {
173
+ [K in keyof L['kinds'] & string]: BoundField<L, K>;
174
+ }[keyof L['kinds'] & string];
175
+ /** What a kayan writes under `targets.service`: its operations, and the store and schema target it runs them through. */
176
+ export type KayanService = {
177
+ /** Every operation this kayan answers; one that declares none answers none. */
178
+ operations?: Record<string, AnyOperation>;
179
+ /** The store this kayan is written through. Defaults to the service target's. */
180
+ store?: string;
181
+ /** The target a write is validated against. Defaults to the service target's. */
182
+ schema?: string;
183
+ };
184
+ /** The three keys a kayan's `targets.service` may carry, in one place. */
185
+ export declare const kayanServiceKeys: readonly ['operations', 'schema', 'store'];
186
+ /**
187
+ * One index over the columns its fields name, in that order; `unique` is `false` when left out.
188
+ * `nullsNotDistinct`, on a unique index, makes rows whose columns are null collide.
189
+ */
190
+ export type CompoundIndex<Field extends string = string> = {
191
+ fields: Field[];
192
+ } & ({
193
+ unique?: boolean;
194
+ nullsNotDistinct?: never;
195
+ } | {
196
+ unique: true;
197
+ nullsNotDistinct?: boolean;
198
+ });
199
+ type GroupEntry<Field extends string> = Field | [Field, ...Field[]];
200
+ type GroupEntries<Field extends string> = [GroupEntry<Field>, ...GroupEntry<Field>[]];
201
+ /**
202
+ * Indexes within a tenant-owned kayan's tenant field, each led by it: `{ within: 'organization',
203
+ * unique: ['title', ['code', 'term']], index: ['startsAt'] }` is unique `(organization, title)`,
204
+ * unique `(organization, code, term)` and plain `(organization, startsAt)`.
205
+ */
206
+ export type IndexGroup<Field extends string = string, Within extends string = string> = {
207
+ within: Within;
208
+ } & ({
209
+ unique: GroupEntries<Field>;
210
+ index?: GroupEntry<Field>[];
211
+ } | {
212
+ unique?: GroupEntry<Field>[];
213
+ index: GroupEntries<Field>;
214
+ });
215
+ /**
216
+ * A kayan's indexes: a list built beside the defaults, or a function of the defaults that answers the whole list.
217
+ * The defaults are one unique index per `unique` field, over the field alone on a shared kayan and over the
218
+ * ownership field and the field on a tenant-owned one. A plain entry is over exactly its fields.
219
+ */
220
+ export type Indexes<Field extends string = string, Within extends string = string> = (CompoundIndex<Field> | IndexGroup<Field, Within>)[] | ((args: {
221
+ defaults: CompoundIndex<Field>[];
222
+ }) => (CompoundIndex<Field> | IndexGroup<Field, Within>)[]);
223
+ export type Kayan = {
224
+ name: string;
225
+ fields: Record<string, RuntimeField>;
226
+ access?: StoredAccess;
227
+ /** The provider that authenticates this kayan: `betterAuth({ server })`, `local({ secret })`, or a strategy the project wrote. */
228
+ auth?: KayanAuth;
229
+ /** The table name. Defaults to the name. */
230
+ dbName?: string;
231
+ /** The Postgres schema this kayan's tables are built in. No key, or `'public'`, is `public`. */
232
+ schemaName?: string;
233
+ /** Indexes over this kayan's own columns; each field named must be one column of its own table. */
234
+ indexes?: (CompoundIndex | IndexGroup)[] | ((args: {
235
+ defaults: never;
236
+ }) => (CompoundIndex | IndexGroup)[]);
237
+ /** One entry per target with something to say about this kayan: `{ drizzle: pgTable(...) }`, `targets.<screen>`, `targets.service`. */
238
+ targets?: Record<string, unknown> & {
239
+ service?: KayanService;
240
+ };
241
+ /** This kayan's own copy, beside the labels its fields carry, nested by the parts of each key. */
242
+ messages?: MessageTree;
243
+ /** Where `mithaq generate messages` writes this kayan's file, if not the default. */
244
+ out?: string;
245
+ /** An open bag the project's own code reads; nothing in mithaq does, and it stays on the server. */
246
+ custom?: Record<string, unknown>;
247
+ /** A kayan of one row, which `defineGlobal` sets: it runs `findOne` and `update`, finds its row without an id and makes it when none. */
248
+ single?: true;
249
+ };
250
+ export type AccessArgs<User = unknown, Data extends Record<string, unknown> = Record<string, unknown>, Id = unknown> = {
251
+ data?: Data;
252
+ id?: Id;
253
+ req?: unknown;
254
+ slug: string;
255
+ user: User | null;
256
+ };
257
+ export type AccessResult = boolean | Where;
258
+ export type Access<User = unknown, Data extends Record<string, unknown> = Record<string, unknown>, Id = unknown> = (args: AccessArgs<User, Data, Id>) => AccessResult | Promise<AccessResult>;
259
+ export type AdminAccessArgs<User = unknown> = {
260
+ req?: unknown;
261
+ user: User;
262
+ };
263
+ export type AdminAccess<User = unknown> = (args: AdminAccessArgs<User>) => boolean | Promise<boolean>;
264
+ export type ActorBoundAccess<Actor> = {
265
+ readonly __mithaqAccessActor: Actor;
266
+ };
267
+ export type ActorBoundFieldAccess<Actor, Doc extends Record<string, unknown>, Id> = FieldAccess<Actor, Doc, Id> & ActorBoundAccess<Actor> & {
268
+ readonly __mithaqFieldData: Doc;
269
+ readonly __mithaqFieldId: Id;
270
+ };
271
+ export declare const fieldAccessFor: <Actor>() => <Doc extends Record<string, unknown>, Id, Fn extends FieldAccess<Actor, Doc, Id> = FieldAccess<Actor, Doc, Id>>(access: Fn) => Fn & ActorBoundFieldAccess<Actor, Doc, Id>;
272
+ type AccessReturnCheck<Map> = {
273
+ [Key in keyof Map]: Map[Key] extends (...args: infer Args) => infer Return ? (...args: Args) => Return extends AccessResult | Promise<AccessResult> ? Return : never : never;
274
+ };
275
+ export declare const accessFor: <Actor>() => <AccessMap extends {
276
+ create?: unknown;
277
+ read?: unknown;
278
+ update?: unknown;
279
+ delete?: unknown;
280
+ }>(access: AccessMap & AccessReturnCheck<AccessMap>) => AccessMap & ActorBoundAccess<Actor>;
281
+ /** A field after `buildAsas` has filled in what the kayan left implicit. */
282
+ export type ResolvedField<F extends RuntimeField = RuntimeField> = F & {
283
+ name: string;
284
+ /** The SQL column name. */
285
+ column: string;
286
+ /** The name of the kayan this field belongs to, which the copy key a refusal carries names. */
287
+ kayan?: string;
288
+ /** The key this build gives the schema target of the field's kayan. */
289
+ schemaTarget?: string;
290
+ /** The language's locales in declared order, on a `localized` field only, so a target can build one thing per locale. */
291
+ locales?: readonly string[];
292
+ /** How this field's kind makes the write's moment, where it declares one. */
293
+ now?: () => unknown;
294
+ /** This field's kind's own implementations, keyed by target type. */
295
+ kindTargets?: Readonly<Record<string, unknown>>;
296
+ /** The `Where` operators this field's kind takes, where it names them. */
297
+ operators?: readonly Operator[];
298
+ /** `false` where this field's kind cannot order a page. */
299
+ sortable?: false;
300
+ /** The kayan of this build that hold files, which a value may point at from inside it. */
301
+ filesIn?: readonly string[];
302
+ /** How a schema target reads back a stored `richText` value: with its shape alone. */
303
+ richText?: (value: unknown) => RichTextRead;
304
+ /** Relationship only: the kayan this field points at. */
305
+ target?: ResolvedKayan;
306
+ };
307
+ export type ResolvedKayan<K extends Kayan = Kayan> = Omit<K, 'indexes' | 'messages' | 'operations'> & {
308
+ /** Every index the store builds: the declared list and the defaults, or what the kayan's function answered. */
309
+ indexes: CompoundIndex[];
310
+ /** The table name: the kayan's `dbName`, or its name. */
311
+ table: string;
312
+ /** The kayan's own copy, flattened by `resolve` to the keys a generated file carries. */
313
+ messages?: MessageMap;
314
+ /** The store and the schema target, the kayan's own or the service target's. */
315
+ store: string;
316
+ schema: string;
317
+ fields: {
318
+ [Name in keyof K['fields']]: ResolvedField<K['fields'][Name]>;
319
+ };
320
+ /** Every operation this kayan declared, and nothing merged in. */
321
+ operations: Record<string, ResolvedOperation>;
322
+ /** The name of the field that marks a row deleted, on a kayan that carries it. */
323
+ softDelete?: string;
324
+ /** The name of the field bound through `targets.tenant`, on a tenant-owned kayan. */
325
+ tenant?: string;
326
+ /** What this kayan keeps of its past, on a kayan that writes the versions target. */
327
+ versions?: ResolvedVersions;
328
+ /** The files this kayan holds and the storage they go through, on a kayan that writes the storage target. */
329
+ storage?: ResolvedStorage;
330
+ /** The schema target's `row` shape, so a caller's `where` is checked against each column's kind before any store is asked. */
331
+ rowShape?: StandardSchemaV1;
332
+ };
333
+ /** One kayan's versions, as `resolve` reads them off its own `targets.<versions>` and the target. */
334
+ export type ResolvedVersions = {
335
+ /** The versions table, `_<table>_versions`. */
336
+ table: string;
337
+ /** That table as a kayan: `parent` a relationship back to this kayan. The `version` column is no field: nothing filters by what a version holds. */
338
+ kayan: ResolvedKayan;
339
+ /** Whether a write with `{ draft: true }` saves a version alone. */
340
+ drafts: boolean;
341
+ /**
342
+ * Whether a draft save with `{ autosave: true }` may overwrite the latest
343
+ * autosave in place, and the interval in milliseconds a screen autosaves at.
344
+ */
345
+ autosave: false | {
346
+ interval: number;
347
+ };
348
+ /** How many versions of one row are kept; `0` keeps every one. */
349
+ max: number;
350
+ };
351
+ export declare const resolvedKayans: WeakMap<object, Readonly<Record<string, ResolvedKayan<Kayan>>>>;
352
+ export declare const sortableFields: (kayan: ResolvedKayan) => string[];
353
+ /**
354
+ * Whether one field cannot order a page: a localized field or a set, a join,
355
+ * a field whose kind says it cannot, or one a screen took out of sorting.
356
+ */
357
+ export declare const unsortable: (field: ResolvedField) => boolean;
358
+ export {};
@@ -0,0 +1,15 @@
1
+ /** The three keys a kayan's `targets.service` may carry, in one place. */
2
+ export const kayanServiceKeys = ['operations', 'schema', 'store'];
3
+ export const fieldAccessFor = () => (access) => access;
4
+ export const accessFor = () => (access) => access;
5
+ // A WeakMap rather than a key on the build, so a browser projection, a JSON
6
+ // serialization and a caller enumerating the namespace all miss it.
7
+ export const resolvedKayans = new WeakMap();
8
+ // One function for the default sort and the copy scan's page, so the rule cannot drift between them.
9
+ export const sortableFields = (kayan) => Object.keys(kayan.fields).filter((name) => !unsortable(kayan.fields[name]));
10
+ /**
11
+ * Whether one field cannot order a page: a localized field or a set, a join,
12
+ * a field whose kind says it cannot, or one a screen took out of sorting.
13
+ */
14
+ export const unsortable = (field) => !!field.rules?.localized || !!field.hasMany || field.kind === 'join' || field.sortable === false
15
+ || Object.values(field.targets ?? {}).some((one) => one?.sortable === false);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A reference to the actor's value at one key. `T` is never written or read; it
3
+ * carries the value's type into the operator the reference is compared with.
4
+ */
5
+ export type Ref<T = unknown> = {
6
+ $user: string;
7
+ __value?: T;
8
+ };
9
+ /** One reference per key of the actor. */
10
+ export type UserRefs<Actor> = {
11
+ readonly [Key in keyof Actor]-?: Ref<NonNullable<Actor[Key]>>;
12
+ };
13
+ /** The actor accessor for a project's own shipped fields, typed by the actor the caller names. */
14
+ export declare const userRefs: <Actor>() => UserRefs<Actor>;
15
+ export declare const user: {
16
+ readonly [key: string]: Ref<never>;
17
+ };
18
+ export declare const isRef: (value: unknown) => value is Ref;
19
+ export declare const refuseLiteral: (value: unknown, where: string) => void;
@@ -0,0 +1,16 @@
1
+ // Frozen, so a row stays data that a query, a form and `JSON.stringify` read without executing anything.
2
+ const refs = new Proxy(Object.freeze({}), {
3
+ // `refs[Symbol.iterator]` must be nothing, or the operand check reads it as a literal.
4
+ get: (_target, key) => (typeof key === 'symbol' ? undefined : Object.freeze({ $user: key })),
5
+ });
6
+ /** The actor accessor for a project's own shipped fields, typed by the actor the caller names. */
7
+ export const userRefs = () => refs;
8
+ export const user = refs;
9
+ export const isRef = (value) => typeof value === 'object' && value !== null && typeof value.$user === 'string';
10
+ // Refused, so a row still holding a `$CURRENT_USER.` string fails loudly rather than resolving to nothing.
11
+ export const refuseLiteral = (value, where) => {
12
+ if (typeof value !== 'string' || !value.startsWith('$CURRENT_USER.'))
13
+ return;
14
+ const key = value.slice('$CURRENT_USER.'.length);
15
+ throw new Error(`${where} names the actor as the string "${value}". That form is gone: write \`user.${key}\`, with the \`user\` \`defineMithaq\` hands back, or \`userRefs<Actor>()\` where the language does not exist yet.`);
16
+ };
@@ -0,0 +1,32 @@
1
+ export type Scalar = string | number | boolean | Date | null;
2
+ export declare const operatorList: readonly ['contains', 'equals', 'exists', 'greater_than', 'greater_than_equal', 'in', 'less_than', 'less_than_equal', 'like', 'not_equals', 'not_in'];
3
+ export type Operator = (typeof operatorList)[number];
4
+ export type FieldWhere = Partial<Record<Operator, Scalar | readonly Scalar[] | boolean>>;
5
+ export type Where = {
6
+ and: readonly Where[];
7
+ } | {
8
+ or: readonly Where[];
9
+ } | {
10
+ [field: string]: FieldWhere;
11
+ };
12
+ export declare const combineWhere: (left?: Where, right?: Where) => Where | undefined;
13
+ /**
14
+ * Whether a `Where` holds, read in memory, with each operator meaning what the
15
+ * stores mean by it. `kindOf` lets a decimal compare by value, as Postgres does.
16
+ */
17
+ export declare const matchesWhere: ({ where, valueOf, kindOf }: {
18
+ where: Where;
19
+ valueOf: (key: string) => unknown;
20
+ kindOf?: (key: string) => string | undefined;
21
+ }) => boolean;
22
+ /**
23
+ * Two stored values the same by what they hold: a moment by its time, an object
24
+ * whatever its key order, an absent value as `null`. No `node:util`: this runs in the browser.
25
+ */
26
+ export declare const same: (left: unknown, right: unknown) => boolean;
27
+ /** Where one value sorts against another, `null` last going up. */
28
+ export declare const compareValues: ({ left, right, direction }: {
29
+ left: unknown;
30
+ right: unknown;
31
+ direction: 'asc' | 'desc';
32
+ }) => number;
@@ -0,0 +1,87 @@
1
+ // Imports nothing but `../base/decimal.ts`, which imports nothing, so `mithaq/admin` can carry it to a browser.
2
+ import { compareDecimals, isDecimal } from '../base/decimal.js';
3
+ // The list is the source and the union is read off it, so the type and the set the runner admits cannot drift.
4
+ export const operatorList = [
5
+ 'contains',
6
+ 'equals',
7
+ 'exists',
8
+ 'greater_than',
9
+ 'greater_than_equal',
10
+ 'in',
11
+ 'less_than',
12
+ 'less_than_equal',
13
+ 'like',
14
+ 'not_equals',
15
+ 'not_in',
16
+ ];
17
+ export const combineWhere = (left, right) => left && right ? { and: [left, right] } : left ?? right;
18
+ /**
19
+ * Whether a `Where` holds, read in memory, with each operator meaning what the
20
+ * stores mean by it. `kindOf` lets a decimal compare by value, as Postgres does.
21
+ */
22
+ export const matchesWhere = ({ where, valueOf, kindOf }) => Object.entries(where).every(([key, rules]) => key === 'and' ? rules.every((one) => matchesWhere({ where: one, valueOf, kindOf }))
23
+ : key === 'or' ? rules.some((one) => matchesWhere({ where: one, valueOf, kindOf }))
24
+ : Object.entries(rules).every(([operator, operand]) => fieldMatch({ value: valueOf(key), operator, operand, decimal: kindOf?.(key) === 'decimal' })));
25
+ /**
26
+ * Two stored values the same by what they hold: a moment by its time, an object
27
+ * whatever its key order, an absent value as `null`. No `node:util`: this runs in the browser.
28
+ */
29
+ export const same = (left, right) => {
30
+ const [a, b] = [left ?? null, right ?? null];
31
+ if (a instanceof Date || b instanceof Date)
32
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
33
+ if (a === b)
34
+ return true;
35
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null || Array.isArray(a) !== Array.isArray(b))
36
+ return false;
37
+ const keys = Object.keys(a);
38
+ return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]));
39
+ };
40
+ const orderValue = (value) => (value instanceof Date ? value.getTime() : value);
41
+ const less = (left, right) => typeof left === 'number' && typeof right === 'number' ? left < right : String(left) < String(right);
42
+ /** Where one value sorts against another, `null` last going up. */
43
+ export const compareValues = ({ left, right, direction }) => {
44
+ if (left === null && right === null)
45
+ return 0;
46
+ if (left === null)
47
+ return direction === 'asc' ? 1 : -1;
48
+ if (right === null)
49
+ return direction === 'asc' ? -1 : 1;
50
+ const a = orderValue(left);
51
+ const b = orderValue(right);
52
+ const order = a === b ? 0 : less(b, a) ? 1 : -1;
53
+ return direction === 'desc' ? -order : order;
54
+ };
55
+ const text = (value) => String(value ?? '').toLowerCase();
56
+ const comparable = (value) => value !== null && value !== undefined;
57
+ const fieldMatch = ({ value, operator, operand, decimal }) => {
58
+ const exact = (left, right) => decimal && isDecimal(left) && isDecimal(right);
59
+ const equal = (left, right) => (exact(left, right) ? compareDecimals(left, right) === 0 : same(left, right));
60
+ const order = (left, right) => (exact(left, right) ? compareDecimals(left, right) : compareValues({ left, right, direction: 'asc' }));
61
+ switch (operator) {
62
+ case 'contains':
63
+ return comparable(value) && text(value).includes(text(operand));
64
+ case 'like':
65
+ return comparable(value) && text(operand).trim().split(/\s+/).filter(Boolean).every((word) => text(value).includes(word));
66
+ case 'equals':
67
+ return equal(value, operand);
68
+ case 'exists':
69
+ return Boolean(operand) ? value !== null && value !== undefined : value === null || value === undefined;
70
+ case 'greater_than':
71
+ return comparable(value) && comparable(operand) && order(value, operand) > 0;
72
+ case 'greater_than_equal':
73
+ return comparable(value) && comparable(operand) && order(value, operand) >= 0;
74
+ case 'in':
75
+ return operand.some((one) => equal(value, one));
76
+ case 'less_than':
77
+ return comparable(value) && comparable(operand) && order(value, operand) < 0;
78
+ case 'less_than_equal':
79
+ return comparable(value) && comparable(operand) && order(value, operand) <= 0;
80
+ case 'not_equals':
81
+ return operand === null ? comparable(value) : comparable(value) && !equal(value, operand);
82
+ case 'not_in':
83
+ return comparable(value) && !operand.some((one) => equal(value, one));
84
+ default:
85
+ return false;
86
+ }
87
+ };
@@ -0,0 +1,105 @@
1
+ import { type CheckedTable, type Held, type RowTable } from '../mithaq/rows.ts';
2
+ import { type Command, type Store } from '../mithaq/language.ts';
3
+ import type { Where } from '../mithaq/where.ts';
4
+ /** One system role, written in code: the copy a screen shows it under and the keys it holds. `'*'` is every key. */
5
+ export type SystemRole = {
6
+ label: Readonly<Record<string, string>>;
7
+ permissions: '*' | readonly string[];
8
+ };
9
+ /** Every system role of a project, slug to role. Its keys are the row keys. */
10
+ export type SystemRoles = Readonly<Record<string, SystemRole>>;
11
+ /** What one actor is assigned in one tenant, and every key those roles hold. */
12
+ export type Assigned = {
13
+ /** `'*'` is every key of the project, which is what a root granter holds. */
14
+ permissions: '*' | readonly string[];
15
+ roles: readonly string[];
16
+ };
17
+ /** The access target: it names the system roles, reads which an actor is assigned, and answers what they hold. */
18
+ export type AccessTarget<Roles extends SystemRoles = SystemRoles, Table extends RowTable = RowTable, Actor = unknown> = {
19
+ readonly capability: 'access';
20
+ /** `'rbac'` from `rbacTarget`, `'betterAuth'` from `betterAuthTarget`. */
21
+ readonly type?: 'rbac' | 'betterAuth';
22
+ /** The system roles, which are also the names a row table may be keyed on. */
23
+ readonly roles: Roles;
24
+ /** The slugs this actor is assigned, read from the trusted snapshot, so an actor naming its own roles is a claim. Absent is `user.roles`. */
25
+ readonly rolesOf?: (user: Actor) => readonly string[];
26
+ /** The rows every tenant-owned kayan inherits. */
27
+ readonly access: Table;
28
+ /** The kayan this target reads rows out of; none may carry a soft-delete mark, or a marked role row would keep granting. */
29
+ readonly reads: readonly string[];
30
+ /**
31
+ * A member's slugs and their keys, in one tenant. `ownership` names the field bound to the
32
+ * tenant on a kayan the target reads, and nothing for a shared one.
33
+ */
34
+ resolve(args: {
35
+ store: Store;
36
+ tenant: unknown;
37
+ member: unknown;
38
+ actor: object;
39
+ ownership: (kayan: string) => string | undefined;
40
+ }): Promise<Assigned>;
41
+ readonly commands?: Record<string, Command>;
42
+ };
43
+ /**
44
+ * The keys an actor holds for a kayan: what the build's access target resolved,
45
+ * or the kayan's own rows where no target answered. Precedence, never a union.
46
+ */
47
+ export declare const heldPermissions: ({ access, user }: {
48
+ access: unknown;
49
+ user: unknown;
50
+ }) => Held;
51
+ /** One page of every row a filter matches, which is how each table is read. */
52
+ export declare const allRows: ({ store, kayan, where }: {
53
+ store: Store;
54
+ kayan: string;
55
+ where: Where;
56
+ }) => Promise<{
57
+ docs: Record<string, unknown>[];
58
+ totalDocs: number;
59
+ }>;
60
+ /** A tenant a row can be filtered by. Nothing else scopes a read, so nothing else reads. */
61
+ export declare const scoped: (value: unknown) => value is number | string;
62
+ /** The actor a command hands an audit sink: the command and the operating-system user who ran it. */
63
+ export type CommandActor = {
64
+ command: string;
65
+ osUser: string;
66
+ };
67
+ export type AccessOptions<Roles extends SystemRoles, Table extends RowTable, Actor> = {
68
+ /** The system roles, which are code and never rows. */
69
+ roles: Roles;
70
+ /** How the actor's assigned slugs are read. Absent is `user.roles`. */
71
+ rolesOf?: (user: Actor) => readonly string[];
72
+ /** The row table, keyed on `'*'` and on the slugs `roles` names. */
73
+ access?: Table & CheckedTable<Table, '*' | (keyof Roles & string)>;
74
+ /** Where the target reads and seeds: `memberRoles` is the assignment, `roles` the custom roles. */
75
+ kayan: {
76
+ memberRoles: string;
77
+ roles: string;
78
+ };
79
+ };
80
+ export declare const assignedSlugs: ({ store, kayan, field, tenant, member }: {
81
+ store: Store;
82
+ kayan: string;
83
+ field: string | undefined;
84
+ tenant: unknown;
85
+ member: unknown;
86
+ }) => Promise<string[]>;
87
+ /**
88
+ * A role slug that is one of the project's own, whose keys are code. The rest
89
+ * are rows, and a target reads them from wherever it holds them.
90
+ */
91
+ export declare const systemKeys: (roles: SystemRoles, slugs: readonly string[]) => {
92
+ custom: string[];
93
+ everything: boolean;
94
+ keys: Set<string>;
95
+ };
96
+ export declare const heldSlugs: ({ roles, rolesOf, slugs, actor }: {
97
+ roles: SystemRoles;
98
+ rolesOf: ((user: never) => readonly string[]) | undefined;
99
+ slugs: readonly string[];
100
+ actor: object | undefined;
101
+ }) => readonly string[];
102
+ /** The access target whose custom roles are rows of a kayan the project ships, beside its system roles in code. */
103
+ export declare const rbacTarget: <const Roles extends SystemRoles, const Table extends RowTable, Actor>({ roles, rolesOf, access, kayan, }: AccessOptions<Roles, Table, Actor>) => AccessTarget<Roles, Table, Actor> & {
104
+ readonly type: 'rbac';
105
+ };