@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,9 @@
1
+ /** Fixed-point text, held to what a Postgres `numeric` holds: 131072 digits before the point and 16383 after. */
2
+ export declare const isDecimal: (value: unknown) => value is string;
3
+ /** How many digits a decimal holds before and after its point, as `numeric(precision, scale)` counts them. */
4
+ export declare const decimalDigits: (value: string) => {
5
+ whole: number;
6
+ fraction: number;
7
+ };
8
+ /** -1, 0 or 1 as `left` is below, equal to or above `right`, on two values `isDecimal` accepts. */
9
+ export declare const compareDecimals: (left: string, right: string) => number;
@@ -0,0 +1,16 @@
1
+ // Read without passing a digit through a JS number, which would round a money
2
+ // value past 15 significant digits. Imports nothing, so a browser can load it.
3
+ /** Fixed-point text, held to what a Postgres `numeric` holds: 131072 digits before the point and 16383 after. */
4
+ export const isDecimal = (value) => typeof value === 'string' && /^[+-]?\d{1,131072}(?:\.\d{1,16383})?$/.test(value);
5
+ /** How many digits a decimal holds before and after its point, as `numeric(precision, scale)` counts them. */
6
+ export const decimalDigits = (value) => {
7
+ const [whole = '', fraction = ''] = value.replace(/^[+-]/, '').split('.');
8
+ return { whole: whole.replace(/^0+/, '').length, fraction: fraction.length };
9
+ };
10
+ /** -1, 0 or 1 as `left` is below, equal to or above `right`, on two values `isDecimal` accepts. */
11
+ export const compareDecimals = (left, right) => {
12
+ const [a, b] = [left, right].map((one) => /^([+-]?\d+)(?:\.(\d+))?$/.exec(one));
13
+ const scale = Math.max(a[2]?.length ?? 0, b[2]?.length ?? 0);
14
+ const [first, second] = [a, b].map((one) => BigInt(one[1] + (one[2] ?? '').padEnd(scale, '0')));
15
+ return first < second ? -1 : first > second ? 1 : 0;
16
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The `id` field alone, for a kayan that keeps no timestamps:
3
+ * `fields: { ...defaultIdField, title }`.
4
+ */
5
+ export declare const defaultIdField: {
6
+ /** A uuid Postgres fills when a create leaves it out; a client may send its own, so rows can be made offline. */
7
+ readonly id: {
8
+ readonly kind: 'uuid';
9
+ readonly label: {
10
+ readonly en: 'ID';
11
+ readonly ar: 'المعرف';
12
+ };
13
+ };
14
+ };
15
+ /** The `updatedAt` and `createdAt` fields the runner writes, spread beside an id. */
16
+ export declare const defaultTimestampFields: {
17
+ /** Written by the runner on every write that changes a row, and drawn disabled in the form. */
18
+ readonly updatedAt: {
19
+ readonly kind: 'datetime';
20
+ readonly label: {
21
+ readonly en: 'Updated at';
22
+ readonly ar: 'تاريخ التعديل';
23
+ };
24
+ readonly rules: {
25
+ readonly required: true;
26
+ readonly defaultNow: true;
27
+ readonly nowOnUpdate: true;
28
+ readonly providedBy: {
29
+ readonly create: 'application';
30
+ readonly update: 'application';
31
+ };
32
+ };
33
+ };
34
+ /** Written by the runner on insert, and indexed because a read that names no sort orders by it. */
35
+ readonly createdAt: {
36
+ readonly kind: 'datetime';
37
+ readonly label: {
38
+ readonly en: 'Created at';
39
+ readonly ar: 'تاريخ الإنشاء';
40
+ };
41
+ readonly rules: {
42
+ readonly required: true;
43
+ readonly defaultNow: true;
44
+ readonly index: true;
45
+ readonly providedBy: {
46
+ readonly create: 'application';
47
+ readonly update: 'application';
48
+ };
49
+ };
50
+ };
51
+ };
52
+ /**
53
+ * The `id`, `updatedAt` and `createdAt` fields a kayan spreads into its own
54
+ * `fields`: `fields: { ...defaultFields, title }`. Spread order is column order.
55
+ */
56
+ export declare const defaultFields: {
57
+ /** A uuid Postgres fills when a create leaves it out; a client may send its own, so rows can be made offline. */
58
+ readonly id: {
59
+ readonly kind: 'uuid';
60
+ readonly label: {
61
+ readonly en: 'ID';
62
+ readonly ar: 'المعرف';
63
+ };
64
+ };
65
+ /** Written by the runner on every write that changes a row, and drawn disabled in the form. */
66
+ readonly updatedAt: {
67
+ readonly kind: 'datetime';
68
+ readonly label: {
69
+ readonly en: 'Updated at';
70
+ readonly ar: 'تاريخ التعديل';
71
+ };
72
+ readonly rules: {
73
+ readonly required: true;
74
+ readonly defaultNow: true;
75
+ readonly nowOnUpdate: true;
76
+ readonly providedBy: {
77
+ readonly create: 'application';
78
+ readonly update: 'application';
79
+ };
80
+ };
81
+ };
82
+ /** Written by the runner on insert, and indexed because a read that names no sort orders by it. */
83
+ readonly createdAt: {
84
+ readonly kind: 'datetime';
85
+ readonly label: {
86
+ readonly en: 'Created at';
87
+ readonly ar: 'تاريخ الإنشاء';
88
+ };
89
+ readonly rules: {
90
+ readonly required: true;
91
+ readonly defaultNow: true;
92
+ readonly index: true;
93
+ readonly providedBy: {
94
+ readonly create: 'application';
95
+ readonly update: 'application';
96
+ };
97
+ };
98
+ };
99
+ };
100
+ export declare const globalTypeLabel: {
101
+ readonly en: 'Global type';
102
+ readonly ar: 'النوع العام';
103
+ };
104
+ /**
105
+ * The field a global spreads into its own `fields`:
106
+ * `fields: { ...defaultFields, siteTitle, ...defaultGlobalFields }`.
107
+ */
108
+ export declare const defaultGlobalFields: {
109
+ readonly globalType: {
110
+ readonly kind: 'text';
111
+ readonly label: {
112
+ readonly en: 'Global type';
113
+ readonly ar: 'النوع العام';
114
+ };
115
+ readonly rules: {
116
+ readonly required: true;
117
+ readonly unique: true;
118
+ };
119
+ readonly access: {
120
+ readonly update: () => false;
121
+ };
122
+ };
123
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The `id` field alone, for a kayan that keeps no timestamps:
3
+ * `fields: { ...defaultIdField, title }`.
4
+ */
5
+ export const defaultIdField = {
6
+ /** A uuid Postgres fills when a create leaves it out; a client may send its own, so rows can be made offline. */
7
+ id: { kind: 'uuid', label: { en: 'ID', ar: 'المعرف' } },
8
+ };
9
+ /** The `updatedAt` and `createdAt` fields the runner writes, spread beside an id. */
10
+ export const defaultTimestampFields = {
11
+ /** Written by the runner on every write that changes a row, and drawn disabled in the form. */
12
+ updatedAt: {
13
+ kind: 'datetime',
14
+ label: { en: 'Updated at', ar: 'تاريخ التعديل' },
15
+ rules: {
16
+ required: true,
17
+ defaultNow: true,
18
+ nowOnUpdate: true,
19
+ providedBy: { create: 'application', update: 'application' },
20
+ },
21
+ },
22
+ /** Written by the runner on insert, and indexed because a read that names no sort orders by it. */
23
+ createdAt: {
24
+ kind: 'datetime',
25
+ label: { en: 'Created at', ar: 'تاريخ الإنشاء' },
26
+ rules: {
27
+ required: true,
28
+ defaultNow: true,
29
+ index: true,
30
+ providedBy: { create: 'application', update: 'application' },
31
+ },
32
+ },
33
+ };
34
+ /**
35
+ * The `id`, `updatedAt` and `createdAt` fields a kayan spreads into its own
36
+ * `fields`: `fields: { ...defaultFields, title }`. Spread order is column order.
37
+ */
38
+ export const defaultFields = { ...defaultIdField, ...defaultTimestampFields };
39
+ export const globalTypeLabel = { en: 'Global type', ar: 'النوع العام' };
40
+ /**
41
+ * The field a global spreads into its own `fields`:
42
+ * `fields: { ...defaultFields, siteTitle, ...defaultGlobalFields }`.
43
+ */
44
+ export const defaultGlobalFields = {
45
+ globalType: {
46
+ kind: 'text',
47
+ label: globalTypeLabel,
48
+ rules: { required: true, unique: true },
49
+ access: { update: () => false },
50
+ },
51
+ };
@@ -0,0 +1,46 @@
1
+ /** The rukn every project lays down first: the shipped kinds and rules. */
2
+ export declare const base: {
3
+ readonly name: "base";
4
+ readonly kinds: {
5
+ text: import("./kinds.ts").Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
6
+ email: import("./kinds.ts").Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
7
+ number: import("./kinds.ts").Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
8
+ integer: import("./kinds.ts").Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
9
+ decimal: import("./kinds.ts").Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "precision" | "providedBy" | "required" | "scale" | "unique">;
10
+ checkbox: import("./kinds.ts").Kind<boolean, "defaultValue" | "index" | "providedBy" | "required">;
11
+ datetime: import("./kinds.ts").Kind<Date, "defaultNow" | "defaultValue" | "index" | "localized" | "nowOnUpdate" | "providedBy" | "required">;
12
+ date: import("./kinds.ts").Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required">;
13
+ textArray: import("./kinds.ts").Kind<string[], "defaultValue" | "maxRows" | "minRows" | "providedBy" | "required">;
14
+ uuid: import("./kinds.ts").Kind<string, "defaultValue" | "index" | "providedBy" | "required" | "unique">;
15
+ relationship: import("./kinds.ts").Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
16
+ pointsAtRow: true;
17
+ };
18
+ join: import("./kinds.ts").Kind<unknown[], "providedBy" | "required">;
19
+ };
20
+ readonly rules: {
21
+ required: import("./rules.ts").Rule<boolean>;
22
+ unique: import("./rules.ts").Rule<boolean>;
23
+ index: import("./rules.ts").Rule<boolean>;
24
+ defaultValue: import("./rules.ts").Rule<string | number | boolean | Date | readonly unknown[] | import("../index.ts").Ref>;
25
+ defaultNow: import("./rules.ts").Rule<boolean>;
26
+ nowOnUpdate: import("./rules.ts").Rule<boolean>;
27
+ providedBy: import("./rules.ts").Rule<Readonly<{
28
+ create?: import("./rules.ts").Provider;
29
+ update?: import("./rules.ts").Provider;
30
+ }>>;
31
+ localized: import("./rules.ts").Rule<boolean>;
32
+ maxLength: import("./rules.ts").Rule<number>;
33
+ precision: import("./rules.ts").Rule<number>;
34
+ scale: import("./rules.ts").Rule<number>;
35
+ minLength: import("./rules.ts").Rule<number>;
36
+ pattern: import("./rules.ts").Rule<RegExp>;
37
+ min: import("./rules.ts").Rule<string | number>;
38
+ max: import("./rules.ts").Rule<string | number>;
39
+ minRows: import("./rules.ts").Rule<number>;
40
+ maxRows: import("./rules.ts").Rule<number>;
41
+ uppercase: import("./rules.ts").Rule<boolean>;
42
+ lowercase: import("./rules.ts").Rule<boolean>;
43
+ trim: import("./rules.ts").Rule<boolean>;
44
+ onDelete: import("./rules.ts").Rule<import("./kinds.ts").OnDelete>;
45
+ };
46
+ };
@@ -0,0 +1,5 @@
1
+ import { defineRukn } from '../mithaq/rukn.js';
2
+ import { defaultKinds } from './kinds.js';
3
+ import { defaultRules } from './rules.js';
4
+ /** The rukn every project lays down first: the shipped kinds and rules. */
5
+ export const base = defineRukn({ name: 'base', kinds: defaultKinds, rules: defaultRules });
@@ -0,0 +1,54 @@
1
+ import type { Localized } from '../mithaq/types.ts';
2
+ import type { Operator } from '../mithaq/where.ts';
3
+ /** A kind of field value: its label, its runtime check, the rules it accepts and what each target makes of it. */
4
+ export type Kind<Value = unknown, RuleName extends string = string> = {
5
+ /** The name of the kind, shown wherever a kind is named to a person. */
6
+ label: Localized;
7
+ /** What a person reads where a value is not of this kind. */
8
+ message: Localized;
9
+ /** Compile-time value carried by fields of this kind; never read at runtime. */
10
+ value: Value;
11
+ /** Runtime check for dynamic field/default input where TypeScript is absent. */
12
+ accepts(value: unknown): value is Value;
13
+ /** Makes the write's moment, for a field that declares `defaultNow` or `nowOnUpdate`. */
14
+ now?: () => Value;
15
+ /** Rules fields of this kind may bind. Target order still decides application order. */
16
+ rules: readonly RuleName[];
17
+ /** What this kind is in each target, keyed by the target's `type`: a column, a shape, a control and a cell. */
18
+ targets?: Readonly<Record<string, unknown>>;
19
+ /** The `Where` operators a field of this kind takes; any other is refused. */
20
+ operators?: readonly Operator[];
21
+ /** `false` where a value of this kind cannot order a page. */
22
+ sortable?: false;
23
+ /** `true` where a value of this kind is the id of another row, named by `relationTo`. */
24
+ pointsAtRow?: true;
25
+ /** What a field of this kind does when the row it points at is deleted, unless it names its own `onDelete`. */
26
+ onDelete?: OnDelete;
27
+ };
28
+ /** Defines a project kind from its runtime check. */
29
+ export declare const kind: <Value>(accepts: (value: unknown) => value is Value) => <const RuleName extends string, const Targets extends Readonly<Record<string, unknown>> = {}, const PointsAtRow extends true | undefined = undefined>(definition: Omit<Kind<Value, RuleName>, 'accepts' | 'value' | 'targets' | 'pointsAtRow'> & {
30
+ targets?: Targets;
31
+ pointsAtRow?: PointsAtRow;
32
+ }) => Kind<Value, RuleName> & ({} extends Targets ? unknown : {
33
+ targets: Targets;
34
+ }) & (PointsAtRow extends true ? {
35
+ pointsAtRow: true;
36
+ } : unknown);
37
+ /** What the database does to this row when the row it points at is deleted. */
38
+ export type OnDelete = 'cascade' | 'restrict' | 'set null';
39
+ export declare const defaultKinds: {
40
+ text: Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
41
+ email: Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
42
+ number: Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
43
+ integer: Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
44
+ decimal: Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "precision" | "providedBy" | "required" | "scale" | "unique">;
45
+ checkbox: Kind<boolean, "defaultValue" | "index" | "providedBy" | "required">;
46
+ datetime: Kind<Date, "defaultNow" | "defaultValue" | "index" | "localized" | "nowOnUpdate" | "providedBy" | "required">;
47
+ date: Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required">;
48
+ textArray: Kind<string[], "defaultValue" | "maxRows" | "minRows" | "providedBy" | "required">;
49
+ uuid: Kind<string, "defaultValue" | "index" | "providedBy" | "required" | "unique">;
50
+ relationship: Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
51
+ pointsAtRow: true;
52
+ };
53
+ join: Kind<unknown[], "providedBy" | "required">;
54
+ };
@@ -0,0 +1,78 @@
1
+ import { isDecimal } from './decimal.js';
2
+ /** Defines a project kind from its runtime check. */
3
+ export const kind = (accepts) => (definition) => ({ ...definition, value: undefined, accepts });
4
+ export const defaultKinds = {
5
+ text: kind((value) => typeof value === 'string')({
6
+ label: { en: 'Text', ar: 'نص' },
7
+ message: { en: 'This is not text.', ar: 'هذا ليس نصا.' },
8
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'maxLength', 'minLength', 'pattern', 'uppercase', 'lowercase', 'trim'],
9
+ }),
10
+ email: kind((value) => typeof value === 'string')({
11
+ label: { en: 'Email', ar: 'بريد إلكتروني' },
12
+ message: { en: 'This is not an email address.', ar: 'هذا ليس بريدا إلكترونيا.' },
13
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'maxLength', 'minLength', 'pattern', 'uppercase', 'lowercase', 'trim'],
14
+ }),
15
+ number: kind((value) => typeof value === 'number')({
16
+ label: { en: 'Number', ar: 'رقم' },
17
+ message: { en: 'This is not a number.', ar: 'هذا ليس رقما.' },
18
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max'],
19
+ }),
20
+ // Held to int4's range, because the column is a Postgres `integer`.
21
+ integer: kind((value) => Number.isInteger(value) && value >= -2147483648 && value <= 2147483647)({
22
+ label: { en: 'Integer', ar: 'عدد صحيح' },
23
+ message: { en: 'This is not a 32-bit integer.', ar: 'هذا ليس عددا صحيحا من 32 بت.' },
24
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max'],
25
+ }),
26
+ // Carried as text, because a JS number would round money past 15 significant digits.
27
+ decimal: kind(isDecimal)({
28
+ label: { en: 'Decimal', ar: 'عدد عشري' },
29
+ message: { en: 'This is not a decimal number.', ar: 'هذا ليس عددا عشريا.' },
30
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max', 'precision', 'scale'],
31
+ }),
32
+ checkbox: kind((value) => typeof value === 'boolean')({
33
+ label: { en: 'Checkbox', ar: 'خانة اختيار' },
34
+ message: { en: 'This is answered yes or no.', ar: 'يجاب على هذا بنعم أو لا.' },
35
+ rules: ['providedBy', 'required', 'index', 'defaultValue'],
36
+ }),
37
+ datetime: kind((value) => value instanceof Date)({
38
+ label: { en: 'Date and time', ar: 'التاريخ والوقت' },
39
+ message: { en: 'This is not a date and time.', ar: 'هذا ليس تاريخا ووقتا.' },
40
+ now: () => new Date(),
41
+ rules: ['providedBy', 'required', 'index', 'defaultValue', 'defaultNow', 'nowOnUpdate', 'localized'],
42
+ }),
43
+ // A string, not a `Date`: a day read in another zone as a moment is the day before.
44
+ // `Date.parse('2026-02-30')` rolls over to March 2, so a day must come back as itself.
45
+ date: kind((value) => typeof value === 'string' &&
46
+ /^\d{4}-\d{2}-\d{2}$/.test(value) &&
47
+ new Date(value).toJSON()?.startsWith(value) === true)({
48
+ label: { en: 'Date', ar: 'التاريخ' },
49
+ message: { en: 'This is not a date.', ar: 'هذا ليس تاريخا.' },
50
+ rules: ['providedBy', 'required', 'index', 'defaultValue', 'localized', 'min', 'max'],
51
+ }),
52
+ textArray: kind((value) => Array.isArray(value) && value.every((one) => typeof one === 'string'))({
53
+ label: { en: 'Text list', ar: 'قائمة نصوص' },
54
+ message: { en: 'This is not a list of text.', ar: 'هذه ليست قائمة نصوص.' },
55
+ rules: ['providedBy', 'required', 'defaultValue', 'minRows', 'maxRows'],
56
+ }),
57
+ // Named rather than `text` because a foreign key takes the type of the id it
58
+ // points at, and Postgres refuses a text reference to a uuid key.
59
+ uuid: kind((value) => typeof value === 'string')({
60
+ label: { en: 'UUID', ar: 'معرف فريد' },
61
+ message: { en: 'This is not a unique identifier.', ar: 'هذا ليس معرفا فريدا.' },
62
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue'],
63
+ }),
64
+ relationship: kind((value) => typeof value === 'number' || typeof value === 'string')({
65
+ label: { en: 'Relationship', ar: 'علاقة' },
66
+ message: { en: 'Pick one of the listed entries.', ar: 'اختر عنصرًا من القائمة.' },
67
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'onDelete'],
68
+ pointsAtRow: true,
69
+ onDelete: 'set null',
70
+ }),
71
+ // The rows of another kayan whose `on` relationship points at this one. It builds
72
+ // no column: the id lives on the child, and a read populates it through the child's access.
73
+ join: kind((value) => Array.isArray(value))({
74
+ label: { en: 'Related rows', ar: 'صفوف مرتبطة' },
75
+ message: { en: 'This is not a list of related entries.', ar: 'هذه ليست قائمة عناصر مرتبطة.' },
76
+ rules: ['providedBy', 'required'],
77
+ }),
78
+ };
@@ -0,0 +1,73 @@
1
+ import { type Ref } from '../mithaq/user.ts';
2
+ import type { Localized } from '../mithaq/types.ts';
3
+ import type { OnDelete } from './kinds.ts';
4
+ export type Rule<Value = unknown> = {
5
+ /** Compile-time value carried by a binding; never read at runtime. */
6
+ value: Value;
7
+ /** Runtime check for dynamic declarations where TypeScript is absent. */
8
+ accepts(value: unknown): value is Value;
9
+ /** What a person reads where this rule refuses a value. A rule that never refuses one carries none. */
10
+ message?: Localized;
11
+ /** The rules a field may not declare beside this one, by name. */
12
+ excludes?: readonly string[];
13
+ };
14
+ export type RuleValue<R> = R extends Rule<infer Value> ? Value : never;
15
+ /** Defines a project rule from its runtime check. */
16
+ export declare const rule: <Value>({ accepts, message, excludes, }: {
17
+ accepts: (value: unknown) => value is Value;
18
+ message?: Localized;
19
+ excludes?: readonly string[];
20
+ }) => Rule<Value>;
21
+ declare const providers: readonly ['client', 'application', 'never'];
22
+ export type Provider = (typeof providers)[number];
23
+ export type ProvidedBy = Readonly<{
24
+ create?: Provider;
25
+ update?: Provider;
26
+ }>;
27
+ /** Who supplies a field's value on one operation; a field that declares nothing is the client's. */
28
+ export declare const providerOf: (field: {
29
+ rules?: Record<string, unknown>;
30
+ }, on: 'create' | 'update') => Provider;
31
+ /** The shipped rules, which a project passes to `defineMithaq` and may add its own to. */
32
+ export declare const defaultRules: {
33
+ required: Rule<boolean>;
34
+ unique: Rule<boolean>;
35
+ index: Rule<boolean>;
36
+ /** A literal, or `user.<key>` for the actor's value. Each row gets its own copy. */
37
+ defaultValue: Rule<string | number | boolean | Date | readonly unknown[] | Ref>;
38
+ /** The moment of the write, on create. */
39
+ defaultNow: Rule<boolean>;
40
+ /** The moment of the write, on every update, whatever the column is called. */
41
+ nowOnUpdate: Rule<boolean>;
42
+ /** Who supplies the value on create and on update. A field declaring nothing is the client's on both. */
43
+ providedBy: Rule<Readonly<{
44
+ create?: Provider;
45
+ update?: Provider;
46
+ }>>;
47
+ /** One value per locale the language declares: one column per locale, and an object keyed by locale in every shape. */
48
+ localized: Rule<boolean>;
49
+ /** Length is a rule, not a kind: `text` with a maximum is `varchar(n)`. */
50
+ maxLength: Rule<number>;
51
+ /** A decimal's size, `numeric(precision, scale)`: declare both or neither. A value with more digits is refused, not rounded. */
52
+ precision: Rule<number>;
53
+ scale: Rule<number>;
54
+ minLength: Rule<number>;
55
+ pattern: Rule<RegExp>;
56
+ /** A bound on an ordered value: a number for `number`, an ISO day for `date`. */
57
+ min: Rule<string | number>;
58
+ max: Rule<string | number>;
59
+ /** How few and how many rows a list holds. A list that is not `required` may be left empty whatever its floor. */
60
+ minRows: Rule<number>;
61
+ maxRows: Rule<number>;
62
+ /** Rewrites the value's case before any check judges it; never refuses a value for its case. */
63
+ uppercase: Rule<boolean>;
64
+ lowercase: Rule<boolean>;
65
+ trim: Rule<boolean>;
66
+ /** What happens to this row when the row it points at is deleted. Left out, a required relationship restricts and an optional one is set null. */
67
+ onDelete: Rule<OnDelete>;
68
+ };
69
+ /** The rule keys a field may carry, derived from the record the language names. */
70
+ export type Rules = {
71
+ [Name in keyof typeof defaultRules]?: RuleValue<(typeof defaultRules)[Name]>;
72
+ };
73
+ export {};
@@ -0,0 +1,80 @@
1
+ import { isRef } from '../mithaq/user.js';
2
+ /** Defines a project rule from its runtime check. */
3
+ export const rule = ({ accepts, message, excludes, }) => ({ value: undefined, accepts, message, excludes });
4
+ const isBoolean = (value) => typeof value === 'boolean';
5
+ const isNumber = (value) => typeof value === 'number';
6
+ const boolean = rule({ accepts: isBoolean });
7
+ const writeOperations = ['create', 'update'];
8
+ // `never` is not `application`: not "something else fills it" but "nobody does, for this operation".
9
+ const providers = ['client', 'application', 'never'];
10
+ /** Who supplies a field's value on one operation; a field that declares nothing is the client's. */
11
+ export const providerOf = (field, on) => field.rules?.providedBy?.[on] ?? 'client';
12
+ /** The shipped rules, which a project passes to `defineMithaq` and may add its own to. */
13
+ export const defaultRules = {
14
+ required: rule({ accepts: isBoolean, message: { en: 'This field is required.', ar: 'هذا الحقل مطلوب.' } }),
15
+ unique: boolean,
16
+ index: boolean,
17
+ /** A literal, or `user.<key>` for the actor's value. Each row gets its own copy. */
18
+ defaultValue: rule({
19
+ accepts: (value) => value instanceof Date || isRef(value) || Array.isArray(value) || ['boolean', 'number', 'string'].includes(typeof value),
20
+ excludes: ['defaultNow'],
21
+ }),
22
+ /** The moment of the write, on create. */
23
+ defaultNow: rule({ accepts: isBoolean, excludes: ['defaultValue', 'localized'] }),
24
+ /** The moment of the write, on every update, whatever the column is called. */
25
+ nowOnUpdate: rule({ accepts: isBoolean, excludes: ['localized'] }),
26
+ /** Who supplies the value on create and on update. A field declaring nothing is the client's on both. */
27
+ providedBy: rule({
28
+ accepts: (value) => !!value &&
29
+ typeof value === 'object' &&
30
+ !Array.isArray(value) &&
31
+ Object.entries(value).every(([operation, supplier]) => writeOperations.includes(operation) && providers.includes(supplier)),
32
+ }),
33
+ /** One value per locale the language declares: one column per locale, and an object keyed by locale in every shape. */
34
+ localized: boolean,
35
+ /** Length is a rule, not a kind: `text` with a maximum is `varchar(n)`. */
36
+ maxLength: rule({ accepts: isNumber, message: { en: 'The value is too long.', ar: 'القيمة طويلة جدًا.' } }),
37
+ /** A decimal's size, `numeric(precision, scale)`: declare both or neither. A value with more digits is refused, not rounded. */
38
+ precision: rule({
39
+ accepts: (value) => Number.isInteger(value) && value >= 1 && value <= 1000,
40
+ message: { en: 'The value has too many digits.', ar: 'القيمة فيها أرقام أكثر من المسموح.' },
41
+ }),
42
+ scale: rule({
43
+ accepts: (value) => Number.isInteger(value) && value >= 0 && value <= 1000,
44
+ message: { en: 'The value has too many decimal places.', ar: 'القيمة فيها منازل عشرية أكثر من المسموح.' },
45
+ }),
46
+ minLength: rule({ accepts: isNumber, message: { en: 'The value is too short.', ar: 'القيمة أقصر من المسموح.' } }),
47
+ pattern: rule({
48
+ accepts: (value) => value instanceof RegExp,
49
+ message: {
50
+ en: 'The value is not in the shape this field takes.',
51
+ ar: 'القيمة ليست بالشكل الذي يقبله هذا الحقل.',
52
+ },
53
+ }),
54
+ /** A bound on an ordered value: a number for `number`, an ISO day for `date`. */
55
+ min: rule({
56
+ accepts: (value) => typeof value === 'number' || typeof value === 'string',
57
+ message: { en: 'The value is too small.', ar: 'القيمة أصغر من المسموح.' },
58
+ }),
59
+ max: rule({
60
+ accepts: (value) => typeof value === 'number' || typeof value === 'string',
61
+ message: { en: 'The value is too large.', ar: 'القيمة أكبر من المسموح.' },
62
+ }),
63
+ /** How few and how many rows a list holds. A list that is not `required` may be left empty whatever its floor. */
64
+ minRows: rule({
65
+ accepts: (value) => Number.isInteger(value) && value >= 0,
66
+ message: { en: 'This list has too few entries.', ar: 'هذه القائمة فيها عناصر أقل من المسموح.' },
67
+ }),
68
+ maxRows: rule({
69
+ accepts: (value) => Number.isInteger(value) && value >= 0,
70
+ message: { en: 'This list has too many entries.', ar: 'هذه القائمة فيها عناصر أكثر من المسموح.' },
71
+ }),
72
+ /** Rewrites the value's case before any check judges it; never refuses a value for its case. */
73
+ uppercase: rule({ accepts: isBoolean, excludes: ['lowercase'] }),
74
+ lowercase: rule({ accepts: isBoolean, excludes: ['uppercase'] }),
75
+ trim: boolean,
76
+ /** What happens to this row when the row it points at is deleted. Left out, a required relationship restricts and an optional one is set null. */
77
+ onDelete: rule({
78
+ accepts: (value) => typeof value === 'string' && ['cascade', 'restrict', 'set null'].includes(value),
79
+ }),
80
+ };