@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.
Files changed (152) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/README.md +160 -2
  6. package/dist/asas/actor.d.ts +96 -0
  7. package/dist/asas/actor.js +114 -0
  8. package/dist/asas/build.d.ts +239 -0
  9. package/dist/asas/build.js +834 -0
  10. package/dist/asas/cli/mithaq.d.ts +2 -0
  11. package/dist/asas/cli/mithaq.js +177 -0
  12. package/dist/asas/cli/upgrade.d.ts +33 -0
  13. package/dist/asas/cli/upgrade.js +256 -0
  14. package/dist/asas/http/fetch.d.ts +41 -0
  15. package/dist/asas/http/fetch.js +178 -0
  16. package/dist/asas/http/orpc.d.ts +45 -0
  17. package/dist/asas/http/orpc.js +153 -0
  18. package/dist/asas/runner/bulk.d.ts +20 -0
  19. package/dist/asas/runner/bulk.js +196 -0
  20. package/dist/asas/runner/errors.d.ts +63 -0
  21. package/dist/asas/runner/errors.js +75 -0
  22. package/dist/asas/runner/read.d.ts +72 -0
  23. package/dist/asas/runner/read.js +601 -0
  24. package/dist/asas/runner/runtime.d.ts +21 -0
  25. package/dist/asas/runner/runtime.js +8 -0
  26. package/dist/asas/runner/steps.d.ts +418 -0
  27. package/dist/asas/runner/steps.js +277 -0
  28. package/dist/asas/runner/store.d.ts +4 -0
  29. package/dist/asas/runner/store.js +21 -0
  30. package/dist/asas/runner/tenant.d.ts +54 -0
  31. package/dist/asas/runner/tenant.js +275 -0
  32. package/dist/asas/runner/write.d.ts +606 -0
  33. package/dist/asas/runner/write.js +1019 -0
  34. package/dist/auth/better-auth/access.d.ts +29 -0
  35. package/dist/auth/better-auth/access.js +103 -0
  36. package/dist/auth/better-auth/generate.d.ts +31 -0
  37. package/dist/auth/better-auth/generate.js +328 -0
  38. package/dist/auth/better-auth/index.d.ts +94 -0
  39. package/dist/auth/better-auth/index.js +81 -0
  40. package/dist/auth/better-auth/tenant.d.ts +26 -0
  41. package/dist/auth/better-auth/tenant.js +34 -0
  42. package/dist/auth/local/index.d.ts +285 -0
  43. package/dist/auth/local/index.js +738 -0
  44. package/dist/base/decimal.d.ts +9 -0
  45. package/dist/base/decimal.js +16 -0
  46. package/dist/base/fields.d.ts +124 -0
  47. package/dist/base/fields.js +52 -0
  48. package/dist/base/index.d.ts +47 -0
  49. package/dist/base/index.js +5 -0
  50. package/dist/base/json-schema.d.ts +7 -0
  51. package/dist/base/json-schema.js +35 -0
  52. package/dist/base/kinds.d.ts +75 -0
  53. package/dist/base/kinds.js +123 -0
  54. package/dist/base/rules.d.ts +73 -0
  55. package/dist/base/rules.js +81 -0
  56. package/dist/drizzle/index.d.ts +308 -0
  57. package/dist/drizzle/index.js +951 -0
  58. package/dist/drizzle/kinds.d.ts +28 -0
  59. package/dist/drizzle/kinds.js +43 -0
  60. package/dist/drizzle/migrate.d.ts +159 -0
  61. package/dist/drizzle/migrate.js +1328 -0
  62. package/dist/drizzle/proxy/index.d.ts +1 -0
  63. package/dist/drizzle/proxy/index.js +1 -0
  64. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  65. package/dist/drizzle/proxy/node-postgres.js +1 -0
  66. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  67. package/dist/drizzle/proxy/pg-core.js +1 -0
  68. package/dist/drizzle/rules.d.ts +336 -0
  69. package/dist/drizzle/rules.js +44 -0
  70. package/dist/index.d.ts +35 -0
  71. package/dist/index.js +26 -0
  72. package/dist/mithaq/define.d.ts +335 -0
  73. package/dist/mithaq/define.js +57 -0
  74. package/dist/mithaq/generate-messages.d.ts +38 -0
  75. package/dist/mithaq/generate-messages.js +235 -0
  76. package/dist/mithaq/language.d.ts +374 -0
  77. package/dist/mithaq/language.js +76 -0
  78. package/dist/mithaq/lock.d.ts +71 -0
  79. package/dist/mithaq/lock.js +329 -0
  80. package/dist/mithaq/message-file.d.ts +13 -0
  81. package/dist/mithaq/message-file.js +29 -0
  82. package/dist/mithaq/messages.d.ts +55 -0
  83. package/dist/mithaq/messages.js +111 -0
  84. package/dist/mithaq/refusal.d.ts +6 -0
  85. package/dist/mithaq/refusal.js +6 -0
  86. package/dist/mithaq/resolve.d.ts +10 -0
  87. package/dist/mithaq/resolve.js +1014 -0
  88. package/dist/mithaq/rows.d.ts +87 -0
  89. package/dist/mithaq/rows.js +174 -0
  90. package/dist/mithaq/rukn.d.ts +72 -0
  91. package/dist/mithaq/rukn.js +15 -0
  92. package/dist/mithaq/standard-schema.d.ts +38 -0
  93. package/dist/mithaq/standard-schema.js +2 -0
  94. package/dist/mithaq/types.d.ts +386 -0
  95. package/dist/mithaq/types.js +15 -0
  96. package/dist/mithaq/user.d.ts +19 -0
  97. package/dist/mithaq/user.js +16 -0
  98. package/dist/mithaq/where.d.ts +32 -0
  99. package/dist/mithaq/where.js +87 -0
  100. package/dist/rbac/index.d.ts +105 -0
  101. package/dist/rbac/index.js +182 -0
  102. package/dist/rbac/steps.d.ts +22 -0
  103. package/dist/rbac/steps.js +161 -0
  104. package/dist/rich-text/index.d.ts +28 -0
  105. package/dist/rich-text/index.js +59 -0
  106. package/dist/rich-text/rich-text.d.ts +106 -0
  107. package/dist/rich-text/rich-text.js +249 -0
  108. package/dist/rich-text/value.d.ts +55 -0
  109. package/dist/rich-text/value.js +126 -0
  110. package/dist/screen/client.d.ts +156 -0
  111. package/dist/screen/client.js +71 -0
  112. package/dist/screen/generate.d.ts +14 -0
  113. package/dist/screen/generate.js +268 -0
  114. package/dist/screen/index.d.ts +496 -0
  115. package/dist/screen/index.js +663 -0
  116. package/dist/screen/react.d.ts +97 -0
  117. package/dist/screen/react.js +58 -0
  118. package/dist/screen/studio.d.ts +24 -0
  119. package/dist/screen/studio.js +68 -0
  120. package/dist/service/index.d.ts +63 -0
  121. package/dist/service/index.js +15 -0
  122. package/dist/storage/allowed-type.d.ts +5 -0
  123. package/dist/storage/allowed-type.js +7 -0
  124. package/dist/storage/index.d.ts +42 -0
  125. package/dist/storage/index.js +43 -0
  126. package/dist/storage/storage.d.ts +386 -0
  127. package/dist/storage/storage.js +412 -0
  128. package/dist/targets.d.ts +14 -0
  129. package/dist/targets.js +16 -0
  130. package/dist/versions/index.d.ts +37 -0
  131. package/dist/versions/index.js +8 -0
  132. package/dist/versions/versions.d.ts +104 -0
  133. package/dist/versions/versions.js +160 -0
  134. package/dist/vite.d.ts +7 -0
  135. package/dist/vite.js +81 -0
  136. package/dist/zod/index.d.ts +193 -0
  137. package/dist/zod/index.js +122 -0
  138. package/dist/zod/kinds.d.ts +36 -0
  139. package/dist/zod/kinds.js +58 -0
  140. package/dist/zod/rules.d.ts +117 -0
  141. package/dist/zod/rules.js +67 -0
  142. package/docs/packages/mithaq/README.md +962 -0
  143. package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
  144. package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
  145. package/docs/packages/mithaq/src/drizzle/README.md +39 -0
  146. package/docs/packages/mithaq-admin/README.md +312 -0
  147. package/docs/templates/asas/README.md +1056 -0
  148. package/package.json +92 -4
  149. package/skills/add-field/SKILL.md +27 -0
  150. package/skills/add-kayan/SKILL.md +26 -0
  151. package/skills/eject-admin-piece/SKILL.md +28 -0
  152. package/skills/upgrade-mithaq/SKILL.md +35 -0
@@ -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,124 @@
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, except that
55
+ * a new table puts the two timestamps after every other column.
56
+ */
57
+ export declare const defaultFields: {
58
+ /** A uuid Postgres fills when a create leaves it out; a client may send its own, so rows can be made offline. */
59
+ readonly id: {
60
+ readonly kind: 'uuid';
61
+ readonly label: {
62
+ readonly en: 'ID';
63
+ readonly ar: 'المعرف';
64
+ };
65
+ };
66
+ /** Written by the runner on every write that changes a row, and drawn disabled in the form. */
67
+ readonly updatedAt: {
68
+ readonly kind: 'datetime';
69
+ readonly label: {
70
+ readonly en: 'Updated at';
71
+ readonly ar: 'تاريخ التعديل';
72
+ };
73
+ readonly rules: {
74
+ readonly required: true;
75
+ readonly defaultNow: true;
76
+ readonly nowOnUpdate: true;
77
+ readonly providedBy: {
78
+ readonly create: 'application';
79
+ readonly update: 'application';
80
+ };
81
+ };
82
+ };
83
+ /** Written by the runner on insert, and indexed because a read that names no sort orders by it. */
84
+ readonly createdAt: {
85
+ readonly kind: 'datetime';
86
+ readonly label: {
87
+ readonly en: 'Created at';
88
+ readonly ar: 'تاريخ الإنشاء';
89
+ };
90
+ readonly rules: {
91
+ readonly required: true;
92
+ readonly defaultNow: true;
93
+ readonly index: true;
94
+ readonly providedBy: {
95
+ readonly create: 'application';
96
+ readonly update: 'application';
97
+ };
98
+ };
99
+ };
100
+ };
101
+ export declare const globalTypeLabel: {
102
+ readonly en: 'Global type';
103
+ readonly ar: 'النوع العام';
104
+ };
105
+ /**
106
+ * The field a global spreads into its own `fields`:
107
+ * `fields: { ...defaultFields, ...defaultGlobalFields, siteTitle }`.
108
+ */
109
+ export declare const defaultGlobalFields: {
110
+ readonly globalType: {
111
+ readonly kind: 'text';
112
+ readonly label: {
113
+ readonly en: 'Global type';
114
+ readonly ar: 'النوع العام';
115
+ };
116
+ readonly rules: {
117
+ readonly required: true;
118
+ readonly unique: true;
119
+ };
120
+ readonly access: {
121
+ readonly update: () => false;
122
+ };
123
+ };
124
+ };
@@ -0,0 +1,52 @@
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, except that
37
+ * a new table puts the two timestamps after every other column.
38
+ */
39
+ export const defaultFields = { ...defaultIdField, ...defaultTimestampFields };
40
+ export const globalTypeLabel = { en: 'Global type', ar: 'النوع العام' };
41
+ /**
42
+ * The field a global spreads into its own `fields`:
43
+ * `fields: { ...defaultFields, ...defaultGlobalFields, siteTitle }`.
44
+ */
45
+ export const defaultGlobalFields = {
46
+ globalType: {
47
+ kind: 'text',
48
+ label: globalTypeLabel,
49
+ rules: { required: true, unique: true },
50
+ access: { update: () => false },
51
+ },
52
+ };
@@ -0,0 +1,47 @@
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
+ json: import("./kinds.ts").Kind<import("./kinds.ts").Json, "defaultValue" | "providedBy" | "required">;
19
+ join: import("./kinds.ts").Kind<unknown[], "providedBy" | "required">;
20
+ };
21
+ readonly rules: {
22
+ required: import("./rules.ts").Rule<boolean>;
23
+ unique: import("./rules.ts").Rule<boolean>;
24
+ index: import("./rules.ts").Rule<boolean>;
25
+ defaultValue: import("./rules.ts").Rule<Date | readonly unknown[] | import("../index.ts").Ref | import("./kinds.ts").Json>;
26
+ defaultNow: import("./rules.ts").Rule<boolean>;
27
+ nowOnUpdate: import("./rules.ts").Rule<boolean>;
28
+ providedBy: import("./rules.ts").Rule<Readonly<{
29
+ create?: import("./rules.ts").Provider;
30
+ update?: import("./rules.ts").Provider;
31
+ }>>;
32
+ localized: import("./rules.ts").Rule<boolean>;
33
+ maxLength: import("./rules.ts").Rule<number>;
34
+ precision: import("./rules.ts").Rule<number>;
35
+ scale: import("./rules.ts").Rule<number>;
36
+ minLength: import("./rules.ts").Rule<number>;
37
+ pattern: import("./rules.ts").Rule<RegExp>;
38
+ min: import("./rules.ts").Rule<string | number>;
39
+ max: import("./rules.ts").Rule<string | number>;
40
+ minRows: import("./rules.ts").Rule<number>;
41
+ maxRows: import("./rules.ts").Rule<number>;
42
+ uppercase: import("./rules.ts").Rule<boolean>;
43
+ lowercase: import("./rules.ts").Rule<boolean>;
44
+ trim: import("./rules.ts").Rule<boolean>;
45
+ onDelete: import("./rules.ts").Rule<import("./kinds.ts").OnDelete>;
46
+ };
47
+ };
@@ -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,7 @@
1
+ import type { ResolvedField } from '../mithaq/types.ts';
2
+ /**
3
+ * Whether a value holds to a `json` field's `jsonSchema`. A schema that names draft-07, as Payload
4
+ * writes one it fetched, is read as draft-07; any other as 2020-12. Throws, naming the field, where
5
+ * the schema cannot be compiled, so a build refuses it rather than checking less than it says.
6
+ */
7
+ export declare const jsonSchemaCheck: (field: ResolvedField) => ((value: unknown) => boolean);
@@ -0,0 +1,35 @@
1
+ // Ajv, not zod's `fromJSONSchema`, which Payload uses: the converter passes over `required` without
2
+ // `properties`, and every keyword of a schema that names no `type`, so a value it waves through would
3
+ // break the schema the project wrote. Ajv holds a value to the whole schema, and refuses one it
4
+ // cannot hold a value to (an unknown keyword or format, a reference it cannot resolve) when it compiles.
5
+ import { Ajv } from 'ajv';
6
+ import { Ajv2020 } from 'ajv/dist/2020.js';
7
+ import addFormats from 'ajv-formats';
8
+ // One compile per schema, though a schema target shapes each field more than once.
9
+ const compiled = new WeakMap();
10
+ /**
11
+ * Whether a value holds to a `json` field's `jsonSchema`. A schema that names draft-07, as Payload
12
+ * writes one it fetched, is read as draft-07; any other as 2020-12. Throws, naming the field, where
13
+ * the schema cannot be compiled, so a build refuses it rather than checking less than it says.
14
+ */
15
+ export const jsonSchemaCheck = (field) => {
16
+ const jsonSchema = field.jsonSchema;
17
+ const held = compiled.get(jsonSchema);
18
+ if (held)
19
+ return held;
20
+ const { schema } = jsonSchema;
21
+ // `strictTypes` and `strictTuples` only ask a schema to say more than JSON Schema requires.
22
+ const options = { strictTypes: false, strictTuples: false };
23
+ const ajv = typeof schema === 'object' && /^http:\/\/json-schema\.org\/draft-07\/schema#?$/.test(String(schema.$schema)) ? new Ajv(options) : new Ajv2020(options);
24
+ addFormats.default(ajv);
25
+ let validate;
26
+ try {
27
+ validate = ajv.compile(schema);
28
+ }
29
+ catch (thrown) {
30
+ throw new Error(`Kayan "${field.kayan}" field "${field.name}" jsonSchema cannot be checked: ${thrown.message}.`, { cause: thrown });
31
+ }
32
+ const check = (value) => validate(value) === true;
33
+ compiled.set(jsonSchema, check);
34
+ return check;
35
+ };
@@ -0,0 +1,75 @@
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
+ /** A value `JSON.parse` could hand back. */
38
+ export type Json = null | boolean | number | string | Json[] | {
39
+ [key: string]: Json;
40
+ };
41
+ /**
42
+ * The most UTF-8 bytes of JSON text a json field holds, which a field names as its `maxBytes`, so one write
43
+ * never stores a value every read of the row then carries.
44
+ */
45
+ export declare const defaultJsonMaxBytes = 1000000;
46
+ /** How deep a json value nests: past it every reader that walks it, this check first, would run out of stack. */
47
+ export declare const jsonDepth = 64;
48
+ /** Text Postgres holds: no NUL, which no `text` or `jsonb` takes, and no half of a surrogate pair. */
49
+ export declare const isStorableText: (text: string) => boolean;
50
+ /**
51
+ * Plain JSON, nested at most `jsonDepth` deep: no `undefined`, no number that is
52
+ * not finite, no `Date`, class instance or function, no cycle, and no string or key
53
+ * Postgres cannot store. A store would otherwise drop, rewrite or refuse it on the
54
+ * way in, and read back something else.
55
+ */
56
+ export declare const isJson: (value: unknown, open?: object[]) => value is Json;
57
+ /** What the database does to this row when the row it points at is deleted. */
58
+ export type OnDelete = 'cascade' | 'restrict' | 'set null';
59
+ export declare const defaultKinds: {
60
+ text: Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
61
+ email: Kind<string, "defaultValue" | "index" | "localized" | "lowercase" | "maxLength" | "minLength" | "pattern" | "providedBy" | "required" | "trim" | "unique" | "uppercase">;
62
+ number: Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
63
+ integer: Kind<number, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required" | "unique">;
64
+ decimal: Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "precision" | "providedBy" | "required" | "scale" | "unique">;
65
+ checkbox: Kind<boolean, "defaultValue" | "index" | "providedBy" | "required">;
66
+ datetime: Kind<Date, "defaultNow" | "defaultValue" | "index" | "localized" | "nowOnUpdate" | "providedBy" | "required">;
67
+ date: Kind<string, "defaultValue" | "index" | "localized" | "max" | "min" | "providedBy" | "required">;
68
+ textArray: Kind<string[], "defaultValue" | "maxRows" | "minRows" | "providedBy" | "required">;
69
+ uuid: Kind<string, "defaultValue" | "index" | "providedBy" | "required" | "unique">;
70
+ relationship: Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
71
+ pointsAtRow: true;
72
+ };
73
+ json: Kind<Json, "defaultValue" | "providedBy" | "required">;
74
+ join: Kind<unknown[], "providedBy" | "required">;
75
+ };
@@ -0,0 +1,123 @@
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
+ /**
5
+ * The most UTF-8 bytes of JSON text a json field holds, which a field names as its `maxBytes`, so one write
6
+ * never stores a value every read of the row then carries.
7
+ */
8
+ export const defaultJsonMaxBytes = 1_000_000;
9
+ /** How deep a json value nests: past it every reader that walks it, this check first, would run out of stack. */
10
+ export const jsonDepth = 64;
11
+ /** Text Postgres holds: no NUL, which no `text` or `jsonb` takes, and no half of a surrogate pair. */
12
+ export const isStorableText = (text) => !text.includes('\u0000') && text.isWellFormed();
13
+ /**
14
+ * Plain JSON, nested at most `jsonDepth` deep: no `undefined`, no number that is
15
+ * not finite, no `Date`, class instance or function, no cycle, and no string or key
16
+ * Postgres cannot store. A store would otherwise drop, rewrite or refuse it on the
17
+ * way in, and read back something else.
18
+ */
19
+ export const isJson = (value, open = []) => {
20
+ if (typeof value === 'string')
21
+ return isStorableText(value);
22
+ if (value === null || typeof value === 'boolean')
23
+ return true;
24
+ if (typeof value === 'number')
25
+ return Number.isFinite(value);
26
+ if (typeof value !== 'object' || open.length >= jsonDepth || open.includes(value))
27
+ return false;
28
+ const prototype = Object.getPrototypeOf(value);
29
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
30
+ return false;
31
+ open.push(value);
32
+ // `Object.values` skips a hole, which `JSON.stringify` would write as `null`, so the length is checked too.
33
+ const plain = (Array.isArray(value) ? Object.keys(value).length === value.length : Object.keys(value).every(isStorableText))
34
+ && Object.values(value).every((one) => isJson(one, open));
35
+ open.pop();
36
+ return plain;
37
+ };
38
+ export const defaultKinds = {
39
+ text: kind((value) => typeof value === 'string')({
40
+ label: { en: 'Text', ar: 'نص' },
41
+ message: { en: 'This is not text.', ar: 'هذا ليس نصا.' },
42
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'maxLength', 'minLength', 'pattern', 'uppercase', 'lowercase', 'trim'],
43
+ }),
44
+ email: kind((value) => typeof value === 'string')({
45
+ label: { en: 'Email', ar: 'بريد إلكتروني' },
46
+ message: { en: 'This is not an email address.', ar: 'هذا ليس بريدا إلكترونيا.' },
47
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'maxLength', 'minLength', 'pattern', 'uppercase', 'lowercase', 'trim'],
48
+ }),
49
+ number: kind((value) => typeof value === 'number')({
50
+ label: { en: 'Number', ar: 'رقم' },
51
+ message: { en: 'This is not a number.', ar: 'هذا ليس رقما.' },
52
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max'],
53
+ }),
54
+ // Held to int4's range, because the column is a Postgres `integer`.
55
+ integer: kind((value) => Number.isInteger(value) && value >= -2147483648 && value <= 2147483647)({
56
+ label: { en: 'Integer', ar: 'عدد صحيح' },
57
+ message: { en: 'This is not a 32-bit integer.', ar: 'هذا ليس عددا صحيحا من 32 بت.' },
58
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max'],
59
+ }),
60
+ // Carried as text, because a JS number would round money past 15 significant digits.
61
+ decimal: kind(isDecimal)({
62
+ label: { en: 'Decimal', ar: 'عدد عشري' },
63
+ message: { en: 'This is not a decimal number.', ar: 'هذا ليس عددا عشريا.' },
64
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'localized', 'min', 'max', 'precision', 'scale'],
65
+ }),
66
+ checkbox: kind((value) => typeof value === 'boolean')({
67
+ label: { en: 'Checkbox', ar: 'خانة اختيار' },
68
+ message: { en: 'This is answered yes or no.', ar: 'يجاب على هذا بنعم أو لا.' },
69
+ rules: ['providedBy', 'required', 'index', 'defaultValue'],
70
+ }),
71
+ datetime: kind((value) => value instanceof Date)({
72
+ label: { en: 'Date and time', ar: 'التاريخ والوقت' },
73
+ message: { en: 'This is not a date and time.', ar: 'هذا ليس تاريخا ووقتا.' },
74
+ now: () => new Date(),
75
+ rules: ['providedBy', 'required', 'index', 'defaultValue', 'defaultNow', 'nowOnUpdate', 'localized'],
76
+ }),
77
+ // A string, not a `Date`: a day read in another zone as a moment is the day before.
78
+ // `Date.parse('2026-02-30')` rolls over to March 2, so a day must come back as itself.
79
+ date: kind((value) => typeof value === 'string' &&
80
+ /^\d{4}-\d{2}-\d{2}$/.test(value) &&
81
+ new Date(value).toJSON()?.startsWith(value) === true)({
82
+ label: { en: 'Date', ar: 'التاريخ' },
83
+ message: { en: 'This is not a date.', ar: 'هذا ليس تاريخا.' },
84
+ rules: ['providedBy', 'required', 'index', 'defaultValue', 'localized', 'min', 'max'],
85
+ }),
86
+ textArray: kind((value) => Array.isArray(value) && value.every((one) => typeof one === 'string'))({
87
+ label: { en: 'Text list', ar: 'قائمة نصوص' },
88
+ message: { en: 'This is not a list of text.', ar: 'هذه ليست قائمة نصوص.' },
89
+ rules: ['providedBy', 'required', 'defaultValue', 'minRows', 'maxRows'],
90
+ }),
91
+ // Named rather than `text` because a foreign key takes the type of the id it
92
+ // points at, and Postgres refuses a text reference to a uuid key.
93
+ uuid: kind((value) => typeof value === 'string')({
94
+ label: { en: 'UUID', ar: 'معرف فريد' },
95
+ message: { en: 'This is not a unique identifier.', ar: 'هذا ليس معرفا فريدا.' },
96
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue'],
97
+ }),
98
+ relationship: kind((value) => typeof value === 'number' || typeof value === 'string')({
99
+ label: { en: 'Relationship', ar: 'علاقة' },
100
+ message: { en: 'Pick one of the listed entries.', ar: 'اختر عنصرًا من القائمة.' },
101
+ rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'onDelete'],
102
+ pointsAtRow: true,
103
+ // A row another still names is never deleted from under it, its drafts included; a field says
104
+ // `set null` or `cascade` to let it go.
105
+ onDelete: 'restrict',
106
+ }),
107
+ // Payload's `json` field, unwrapped, so each reads a row the other wrote. Nothing orders by one:
108
+ // jsonb's order is no order a person reads.
109
+ json: kind(isJson)({
110
+ label: { en: 'JSON', ar: 'JSON' },
111
+ message: { en: 'This is not JSON this field takes.', ar: 'هذه ليست بيانات JSON يقبلها هذا الحقل.' },
112
+ rules: ['providedBy', 'required', 'defaultValue'],
113
+ operators: ['equals', 'not_equals', 'in', 'not_in', 'exists', 'like', 'contains'],
114
+ sortable: false,
115
+ }),
116
+ // The rows of another kayan whose `on` relationship points at this one. It builds
117
+ // no column: the id lives on the child, and a read populates it through the child's access.
118
+ join: kind((value) => Array.isArray(value))({
119
+ label: { en: 'Related rows', ar: 'صفوف مرتبطة' },
120
+ message: { en: 'This is not a list of related entries.', ar: 'هذه ليست قائمة عناصر مرتبطة.' },
121
+ rules: ['providedBy', 'required'],
122
+ }),
123
+ };
@@ -0,0 +1,73 @@
1
+ import { type Ref } from '../mithaq/user.ts';
2
+ import type { Localized } from '../mithaq/types.ts';
3
+ import { type Json, 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, JSON for a `json` field, or `user.<key>` for the actor's value. Each row gets its own copy. */
37
+ defaultValue: Rule<Date | readonly unknown[] | Ref | Json>;
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 field and a set restrict and any other takes its kind's default. */
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 {};