@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,832 @@
1
+ import { and, asc, count as sqlCount, desc, eq, getTableColumns, getTableName, gt, gte, ilike, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from 'drizzle-orm';
2
+ import { drizzle } from 'drizzle-orm/node-postgres';
3
+ import { boolean as pgBoolean, index, integer as pgInteger, isPgEnum, jsonb, timestamp, uuid as pgUuid, pgEnum, pgSchema, pgTable, primaryKey, unique as uniqueConstraint, uniqueIndex, } from 'drizzle-orm/pg-core';
4
+ import { isPlainIdentifier, snakeCase, uniqueRuleIndexes, versionsTableOf } from '../mithaq/resolve.js';
5
+ import { implementationOf } from '../mithaq/rukn.js';
6
+ import { conflict, localizedQuery } from '../asas/runner/errors.js';
7
+ import { Refusal } from '../mithaq/refusal.js';
8
+ import { providerOf } from '../base/rules.js';
9
+ import { idKinds as drizzleIdKinds, kinds as drizzleKinds } from './kinds.js';
10
+ import { rules as drizzleRules } from './rules.js';
11
+ const modifiable = (column) => {
12
+ for (const method of ['default', 'notNull', 'primaryKey', 'unique']) {
13
+ if (typeof Reflect.get(column, method) !== 'function') {
14
+ throw new Error(`The drizzle column builder has no ${method} modifier.`);
15
+ }
16
+ }
17
+ return column;
18
+ };
19
+ // A localized field is one column per locale, `<column>_<locale>`, each the same field
20
+ // renamed, so every builder and rule runs per locale. An override is its one column.
21
+ const columnsOfField = (store) => (field) =>
22
+ // A `hasMany` field's set lives in its join table, and a `join` field's id is the child's column.
23
+ field.hasMany || field.kind === 'join'
24
+ ? []
25
+ : !field.rules?.localized || field.targets?.[store]
26
+ ? [field]
27
+ : field.locales.map((locale) => ({
28
+ ...field,
29
+ name: `${field.name}_${locale}`,
30
+ column: `${field.column}_${locale}`,
31
+ }));
32
+ // One table per `hasMany` field, `courses_tags`, the same string as its SQL name and
33
+ // its key on `tables`, so the push and `mithaq migrate` reach it unchanged.
34
+ export const joinTableOf = (kayan, field) => `${kayan.table}_${snakeCase(field.name)}`;
35
+ /** The fields of one kayan the store holds as a join table rather than a column. */
36
+ export const manyFields = (kayan) => Object.values(kayan?.fields ?? {}).filter((field) => field.hasMany);
37
+ // Read off the global symbol drizzle keeps it under, so an override that is no
38
+ // table answers `public` and is refused by name rather than as a `TypeError`.
39
+ export const schemaOf = (table) => table?.[Symbol.for('drizzle:Schema')] ?? 'public';
40
+ // A lowercase plain identifier needs no quoting anywhere and names the schema module's
41
+ // export. Postgres keeps `pg_*` and `information_schema`; mithaq's history is in `drizzle`.
42
+ const refusedSchema = (name) => typeof name !== 'string' || !isPlainIdentifier(name) || name !== name.toLowerCase()
43
+ ? 'which is not a lowercase SQL identifier: lowercase letters, digits and underscores, not starting with a digit, at most 63 bytes.'
44
+ : name.startsWith('pg_') || name === 'information_schema'
45
+ ? 'which Postgres keeps for itself.'
46
+ : name === migrationsTable.schema
47
+ ? 'where mithaq keeps its migration history.'
48
+ : undefined;
49
+ // A plain `Error`: what reaches here is a `where` an access callback returned, the
50
+ // project's own bug; the read layer refuses a caller's first.
51
+ const refuseLocalized = (kayan, field, what, locales) => new Error(localizedQuery({ kayan, field, what, locales }));
52
+ // Postgres answers `23503`, refused as a conflict naming which kayan points at the
53
+ // row, never which row. Read by schema too: another tool may share a table name.
54
+ const referenced = ({ thrown, name, kayans, tables }) => {
55
+ const held = thrown;
56
+ const error = held?.code === '23503' ? held : held?.cause?.code === '23503' ? held.cause : undefined;
57
+ if (!error)
58
+ return thrown;
59
+ // A kayan's join tables are built in its own table's schema.
60
+ const by = [...kayans].find(([key, kayan]) => schemaOf(tables[key]) === error.schema &&
61
+ (kayan.table === error.table || manyFields(kayan).some((field) => joinTableOf(kayan, field) === error.table)))?.[1].name;
62
+ return conflict({ message: `A "${name}" row is still pointed at by another row, so it is not deleted.`, key: 'error.conflict.referenced', params: { kayan: name, ...(by && { by }) }, cause: thrown });
63
+ };
64
+ // Read by SQLSTATE `23505` on the driver error's cause, not by its locale-dependent
65
+ // message; every other failure is rethrown as it was.
66
+ // The cause's `detail` names the collided value, so it must never be serialized.
67
+ const collision = (thrown, name) => {
68
+ const held = thrown;
69
+ if (held?.code !== '23505' && held?.cause?.code !== '23505')
70
+ return thrown;
71
+ return conflict({
72
+ message: `A "${name}" row already holds one of the unique values this write carries.`,
73
+ key: 'error.conflict.unique',
74
+ params: { kayan: name },
75
+ cause: thrown,
76
+ });
77
+ };
78
+ // Every `text` member of Lexical's tree as one JSON array, so a search runs over what
79
+ // a reader sees, never a key or a node type.
80
+ const textOf = (column) => sql `jsonb_path_query_array(${column}, 'strict $.**.text')::text`;
81
+ const pattern = (value) => `%${value.replace(/[\\%_]/g, '\\$&')}%`;
82
+ const splitNulls = (value) => {
83
+ const values = value;
84
+ return { hasNull: values.some((one) => one === null), values: values.filter((one) => one !== null) };
85
+ };
86
+ // The column is the caller's: this table's, or a join table's for a `hasMany` field.
87
+ const conditionsOf = (column, rules) => {
88
+ const checks = Object.entries(rules).map(([operator, value]) => {
89
+ switch (operator) {
90
+ case 'contains':
91
+ return ilike(column, pattern(String(value)));
92
+ case 'like':
93
+ return and(...String(value).trim().split(/\s+/).filter(Boolean).map((word) => ilike(column, pattern(word))));
94
+ case 'equals':
95
+ return value === null ? isNull(column) : eq(column, value);
96
+ case 'exists':
97
+ return value ? isNotNull(column) : isNull(column);
98
+ case 'greater_than':
99
+ return gt(column, value);
100
+ case 'greater_than_equal':
101
+ return gte(column, value);
102
+ case 'in': {
103
+ const split = splitNulls(value);
104
+ const nonNull = split.values.length ? inArray(column, split.values) : sql `false`;
105
+ return split.hasNull ? or(isNull(column), nonNull) : nonNull;
106
+ }
107
+ case 'less_than':
108
+ return lt(column, value);
109
+ case 'less_than_equal':
110
+ return lte(column, value);
111
+ case 'not_equals':
112
+ return value === null ? isNotNull(column) : ne(column, value);
113
+ case 'not_in': {
114
+ const split = splitNulls(value);
115
+ const nonNull = split.values.length ? notInArray(column, split.values) : sql `true`;
116
+ return split.hasNull ? and(isNotNull(column), nonNull) : nonNull;
117
+ }
118
+ }
119
+ // Unreachable, but a value past both checks would otherwise be left out of a `where`,
120
+ // which carries an access constraint.
121
+ throw new Error(`Unsupported where operator "${operator}".`);
122
+ });
123
+ return checks.length === 1 ? checks[0] : and(...checks);
124
+ };
125
+ const whereOf = (db, tables, kayanByName, name, where) => {
126
+ const table = tables[name];
127
+ const kayan = kayanByName.get(name);
128
+ const parts = [];
129
+ const conjunction = where.and;
130
+ if (Array.isArray(conjunction))
131
+ parts.push(and(...conjunction.map((one) => whereOf(db, tables, kayanByName, name, one))));
132
+ const disjunction = where.or;
133
+ if (Array.isArray(disjunction))
134
+ parts.push(or(...disjunction.map((one) => whereOf(db, tables, kayanByName, name, one))));
135
+ for (const [field, rules] of Object.entries(where)) {
136
+ if (field === 'and' || field === 'or')
137
+ continue;
138
+ const [ownField, relatedField] = field.split('.');
139
+ const relationField = kayan.fields[ownField];
140
+ // A `hasMany` field has no column here, so the join table is filtered and its
141
+ // parent ids filter this table.
142
+ if (relationField?.hasMany) {
143
+ const join = tables[joinTableOf(kayan, relationField)];
144
+ const target = tables[relationField.target.name];
145
+ const column = relatedField ? target[relatedField] : join.target;
146
+ const own = conditionsOf(column, rules);
147
+ const parents = relatedField
148
+ ? db.select({ id: join.parent }).from(join).where(inArray(join.target, db.select({ id: target.id }).from(target).where(own)))
149
+ : db.select({ id: join.parent }).from(join).where(own);
150
+ parts.push(inArray(table.id, parents));
151
+ continue;
152
+ }
153
+ // A locale named as the second segment is one column of this same table.
154
+ const locale = relatedField && relationField?.locales?.includes(relatedField) && !relationField.targets?.[kayan.store]
155
+ ? `${ownField}_${relatedField}`
156
+ : undefined;
157
+ const hop = locale ? undefined : relatedField;
158
+ const relationTo = relationField?.relationTo;
159
+ const relationName = relationField?.target?.name ?? (typeof relationTo === 'function' ? relationTo().name : relationTo);
160
+ // A localized field named alone is several columns, and nothing here may pick a locale.
161
+ // Asked of the field the path lands on, which across a hop is the related kayan's.
162
+ const named = hop ? (relationField?.target ? relationField.target.fields[hop] : relationField) : relationField;
163
+ if (!locale && named?.rules?.localized) {
164
+ throw refuseLocalized(hop && relationField?.target ? relationName : name, named.name, 'filter', named.locales);
165
+ }
166
+ // Only a direct store call or a language's own `authorize` reaches here; the
167
+ // lookup below would otherwise index `tables[undefined]`.
168
+ if (hop && !relationField?.target) {
169
+ throw new Error(`Kayan "${name}" field "${ownField}" names no relationship, so the path "${field}" reaches no second table.`);
170
+ }
171
+ // Typed as drizzle's comparison operators take, so `eq` resolves its untyped overload.
172
+ const column = hop ? tables[relationName][hop] : table[locale ?? field];
173
+ const own = conditionsOf(named?.kind === 'richText' ? textOf(column) : column, rules);
174
+ if (!hop) {
175
+ parts.push(own);
176
+ continue;
177
+ }
178
+ const target = tables[relationName];
179
+ const ids = db.select({ id: target.id }).from(target).where(own);
180
+ parts.push(inArray(table[ownField], ids));
181
+ }
182
+ return parts.length === 1 ? parts[0] : and(...parts);
183
+ };
184
+ const idWhere = (db, tables, kayanByName, name, id, where) => {
185
+ const table = tables[name];
186
+ const byId = eq(table.id, id);
187
+ return where ? and(byId, whereOf(db, tables, kayanByName, name, where)) : byId;
188
+ };
189
+ // A name no migration folder can carry: drizzle matches a folder to a history row by
190
+ // `name` alone. It refuses the first migration against a database only ever pushed.
191
+ export const devPushMarker = 'mithaq_dev_push';
192
+ // A table of mithaq's own, so a default history table holding another tool's rows is
193
+ // never read as this project's.
194
+ export const migrationsTable = { schema: 'drizzle', table: '__mithaq_migrations' };
195
+ // drizzle-kit filters tables by name across every schema in its filter, so a push would
196
+ // drop another tool's table sharing a name with ours, rows and all. Refused by name first.
197
+ export const refuseStrangers = async ({ db, tables, by }) => {
198
+ const built = Object.values(tables);
199
+ if (!built.length)
200
+ return;
201
+ const own = new Set(built.map((table) => `${schemaOf(table)}.${getTableName(table)}`));
202
+ const listed = (values) => sql.join([...new Set(values)].map((value) => sql `${value}`), sql `, `);
203
+ const { rows } = await db.execute(sql `select nspname, relname from pg_catalog.pg_class
204
+ join pg_catalog.pg_namespace on pg_namespace.oid = relnamespace
205
+ where relkind in ('r', 'p', 'v', 'm')
206
+ and nspname in (${listed(built.map(schemaOf))})
207
+ and relname in (${listed(built.map((table) => getTableName(table)))})`);
208
+ const found = rows.map((row) => `${row.nspname}.${row.relname}`).filter((pair) => !own.has(pair)).sort();
209
+ if (!found.length)
210
+ return;
211
+ const one = found.length === 1;
212
+ throw new Refusal(`${found.join(', ')} ${one ? 'is not a table' : 'are not tables'} of this language, and drizzle-kit filters tables by name across the schemas this language uses, so ${by === 'push' ? 'this push' : 'the first migration after this baseline'} would drop ${one ? 'it' : 'them'}. Rename the kayan's table, or move the other ${one ? 'table' : 'tables'}. Where ${one ? 'it is' : 'one is'} this language's own table from before a kayan's schemaName changed, run \`mithaq migrate\`, which asks whether that is a move; on a development database, drop its tables instead, or move the table by hand with \`ALTER TABLE … SET SCHEMA\`. Nothing was written.`);
213
+ };
214
+ const enumsOf = (tables) => {
215
+ const enums = Object.create(null);
216
+ for (const table of Object.values(tables)) {
217
+ for (const { enum: used } of Object.values(getTableColumns(table))) {
218
+ if (isPgEnum(used))
219
+ enums[`${used.schema ?? 'public'}.${used.enumName}`] = used;
220
+ }
221
+ }
222
+ return enums;
223
+ };
224
+ // Each schema and every enum already in those schemas is handed over: drizzle-kit
225
+ // otherwise plans a `DROP` for what it is not handed, or throws asking about a rename.
226
+ export const pushInput = ({ tables, schemas, held = [] }) => {
227
+ const enums = new Map(held.map(({ schema, name, labels }) => [`${schema}.${name}`, schema === 'public' ? pgEnum(name, labels) : schemas[schema].enum(name, labels)]));
228
+ for (const [key, used] of Object.entries(enumsOf(tables))) {
229
+ const schema = used.schema ?? 'public';
230
+ if (!(schema in schemas))
231
+ throw new Error(`A column uses the enum "${key}", in a schema no kayan is built in, so the push could not tell on its second run that it exists. Build the enum in the schemaName of a kayan.`);
232
+ const other = held.find((one) => one.schema === schema && one.name === used.enumName);
233
+ if (other && !other.owned && other.labels.join() !== used.enumValues.join()) {
234
+ throw new Error(`The enum "${schema}"."${used.enumName}" is already in the database with the labels ${other.labels.join(', ')}, and no table of this language uses it, so it is another tool's. Giving it the labels ${used.enumValues.join(', ')} would alter it. Give the column's enum another name or, if it is this language's own, left behind by a dropped table, drop the type with DROP TYPE "${schema}"."${used.enumName}".`);
235
+ }
236
+ enums.set(key, used);
237
+ }
238
+ return {
239
+ imports: {
240
+ ...tables,
241
+ ...Object.fromEntries(Object.entries(schemas).flatMap(([name, schema]) => (schema ? [[`${name}Schema`, schema]] : []))),
242
+ ...Object.fromEntries([...enums].map(([key, one]) => [`${key}Enum`, one])),
243
+ },
244
+ filter: {
245
+ schemas: Object.keys(schemas),
246
+ tables: Object.values(tables).map((table) => getTableName(table)),
247
+ entities: undefined,
248
+ extensions: [],
249
+ },
250
+ };
251
+ };
252
+ /** The history table as SQL, for the statements that read and write it. */
253
+ export const historyTable = sql `${sql.identifier(migrationsTable.schema)}.${sql.identifier(migrationsTable.table)}`;
254
+ /**
255
+ * The one lock every writer of the history takes, a production boot running drizzle's migrator
256
+ * and a connect making the table, since both create the `drizzle` schema. Keyed as a boot has
257
+ * always keyed it, so an instance of an older release booting beside this one still waits.
258
+ */
259
+ export const historyLock = sql `select pg_advisory_xact_lock(hashtext(${`${migrationsTable.schema}.${migrationsTable.table}`}))`;
260
+ // Under an advisory lock, because two connects against a fresh database both pass `if not exists`
261
+ // and the loser fails on Postgres's unique index over schema names. The DDL is drizzle's own,
262
+ // because mithaq writes into the table before drizzle does; any other shape would make the next
263
+ // `drizzle-kit migrate` refuse every row.
264
+ export const createHistory = (db) => db.transaction(async (tx) => {
265
+ await tx.execute(historyLock);
266
+ await tx.execute(sql `create schema if not exists ${sql.identifier(migrationsTable.schema)}`);
267
+ await tx.execute(sql `create table if not exists ${historyTable} (
268
+ id serial primary key,
269
+ hash text not null,
270
+ created_at bigint,
271
+ name text,
272
+ applied_at timestamp with time zone default now()
273
+ )`);
274
+ });
275
+ // One statement, so a run killed between a delete and an insert cannot leave the table
276
+ // with neither: a data-modifying `with` runs once and to completion.
277
+ export const recordMigration = async ({ db, hash, createdAt, name }) => {
278
+ await createHistory(db);
279
+ await db.execute(sql `with gone as (delete from ${historyTable} where name = ${devPushMarker})
280
+ insert into ${historyTable} (hash, created_at, name) values (${hash}, ${createdAt}, ${name})`);
281
+ };
282
+ /** The drizzle store target's maps: a map per kind and a map per rule. */
283
+ export const drizzleTarget = { capability: 'store', type: 'drizzle', idKinds: drizzleIdKinds, kinds: drizzleKinds, rules: drizzleRules };
284
+ export const drizzleAdapter = ({ db: ready, connectionString, kinds, rules, idKinds = drizzleIdKinds, push: named, prodMigrations, }) => {
285
+ if (prodMigrations && prodMigrations.pending !== 'run' && prodMigrations.pending !== 'refuse') {
286
+ throw new Error(`drizzleAdapter's prodMigrations.pending is 'run' or 'refuse', not ${JSON.stringify(prodMigrations.pending)}.`);
287
+ }
288
+ // `prodMigrations` is read only where the push is off, so beside `push: true` it would be unread.
289
+ if (named === true && prodMigrations) {
290
+ throw new Error("drizzleAdapter takes `push: true` or `prodMigrations`, never both: a production connect() reads prodMigrations only with the push off. Leave `push` out, which pushes outside production alone.");
291
+ }
292
+ const push = named ?? process.env.NODE_ENV !== 'production';
293
+ if (ready && connectionString) {
294
+ throw new Error('drizzleAdapter takes a `db` or a `connectionString`, never both.');
295
+ }
296
+ if (!ready && !connectionString) {
297
+ throw new Error('drizzleAdapter needs either a `db` or a `connectionString`.');
298
+ }
299
+ // Made here, not in `init`, so `asas.targets.drizzle.db` is reachable from the start;
300
+ // node-postgres opens no socket until the first query.
301
+ const opened = connectionString ? drizzle(connectionString) : undefined;
302
+ const db = ready ?? opened;
303
+ const config = { ready, opened, db, connectionString, kinds, rules, idKinds, push, prodMigrations };
304
+ // Each build gets a store of its own, so a refused or renamed kayan of one build is
305
+ // never in the next. Every one shares the one client.
306
+ const every = stateOf();
307
+ // A push or a diff of the merged tables would build or keep a table no build has.
308
+ const merged = () => {
309
+ throw new Refusal('The drizzle store the language holds keeps every build\'s tables merged, an earlier build\'s among them, so it neither pushes nor migrates them: connect a build, `asas.connect()`, or run the command on `asas.targets.drizzle`.');
310
+ };
311
+ const adapter = { ...storeOf({ ...config, state: every }), connect: async () => merged(), commands: { migrate: async () => merged() } };
312
+ adapter.init = function (kayanList) {
313
+ // A target that spreads this adapter and overrides a member would hand the build a store
314
+ // without the override: refused rather than dropped in silence.
315
+ if (this !== undefined && this !== adapter) {
316
+ throw new Error('A target wraps the drizzle store and not its init, and each build uses the store init returns, so what the wrapper overrides would reach no build. Return the wrapped store from init too.');
317
+ }
318
+ const state = stateOf();
319
+ const built = storeOf({ ...config, state });
320
+ built.init(kayanList);
321
+ Object.assign(every.tables, state.tables);
322
+ Object.assign(every.schemas, state.schemas);
323
+ Object.assign(every.enums, state.enums);
324
+ for (const [name, kayan] of state.kayanByName)
325
+ every.kayanByName.set(name, kayan);
326
+ return built;
327
+ };
328
+ return adapter;
329
+ };
330
+ /** What one store builds and reads by: its tables, their schemas and enums, and its kayan by table key. */
331
+ const stateOf = () => ({
332
+ // No prototype, so no kayan or join table name finds an inherited answer.
333
+ tables: Object.create(null),
334
+ schemas: Object.create(null),
335
+ enums: Object.create(null),
336
+ kayanByName: new Map(),
337
+ });
338
+ const storeOf = ({ ready, opened, db, connectionString, kinds, rules, idKinds, push, prodMigrations, state }) => {
339
+ const { tables, schemas, enums, kayanByName } = state;
340
+ // `pgSchema('public')` throws, so `public` is `pgTable`. A kayan's join and versions
341
+ // tables are built in its own table's schema.
342
+ const tableIn = (schemaName) => schemaName === undefined || schemaName === 'public' ? pgTable : (schemas[schemaName] ??= pgSchema(schemaName)).table;
343
+ /** The fields of one kayan that this store holds as several columns. */
344
+ const localizedOf = (name) => {
345
+ const kayan = kayanByName.get(name);
346
+ return Object.values(kayan?.fields ?? {}).filter((field) => field.rules?.localized && !field.targets?.[kayan.store]);
347
+ };
348
+ // A field the write names is written whole, every locale column of it, so no write reads the row first.
349
+ const flatten = (name, values) => {
350
+ const fields = localizedOf(name);
351
+ if (!fields.length)
352
+ return values;
353
+ const flat = { ...values };
354
+ for (const field of fields) {
355
+ if (!(field.name in flat))
356
+ continue;
357
+ const given = flat[field.name];
358
+ delete flat[field.name];
359
+ for (const locale of field.locales)
360
+ flat[`${field.name}_${locale}`] = given?.[locale] ?? null;
361
+ }
362
+ return flat;
363
+ };
364
+ // A field whose every locale column is null is one null, which matches the derived row shape.
365
+ const expand = (name, row) => {
366
+ const fields = localizedOf(name);
367
+ if (!fields.length)
368
+ return row;
369
+ const held = { ...row };
370
+ for (const field of fields) {
371
+ const keys = field.locales.map((locale) => `${field.name}_${locale}`);
372
+ if (!keys.some((key) => key in held))
373
+ continue;
374
+ const value = Object.fromEntries(field.locales.map((locale, at) => [locale, held[keys[at]] ?? null]));
375
+ for (const key of keys)
376
+ delete held[key];
377
+ held[field.name] = Object.values(value).every((one) => one === null) ? null : value;
378
+ }
379
+ return held;
380
+ };
381
+ // A field the write does not name is left alone, so a soft delete never touches a set.
382
+ const withoutSets = (name, values) => {
383
+ const fields = manyFields(kayanByName.get(name));
384
+ if (!fields.length)
385
+ return { columns: values, sets: {} };
386
+ const columns = { ...values };
387
+ const sets = Object.create(null);
388
+ for (const field of fields) {
389
+ if (!(field.name in columns))
390
+ continue;
391
+ const given = columns[field.name];
392
+ delete columns[field.name];
393
+ // `null` is the empty set written out.
394
+ sets[field.name] = Array.isArray(given) ? given : [];
395
+ }
396
+ return { columns, sets };
397
+ };
398
+ // The whole array replaces the set. Deduplicated: two join rows for one pair would read
399
+ // back as the id twice.
400
+ const writeSets = async (tx, name, id, sets) => {
401
+ const kayan = kayanByName.get(name);
402
+ for (const [field, given] of Object.entries(sets)) {
403
+ const join = tables[joinTableOf(kayan, kayan.fields[field])];
404
+ await tx.delete(join).where(eq(join.parent, id));
405
+ const values = [...new Set(given)].map((target, at) => ({ parent: id, target, order: at }));
406
+ if (values.length)
407
+ await tx.insert(join).values(values);
408
+ }
409
+ };
410
+ // One query per `hasMany` field however many rows are asked about. A row with no join
411
+ // row gets the empty array, so the key is always there.
412
+ const readSets = async (client, name, ids) => {
413
+ const kayan = kayanByName.get(name);
414
+ const fields = manyFields(kayan);
415
+ const held = new Map(ids.map((id) => [String(id), Object.fromEntries(fields.map((field) => [field.name, []]))]));
416
+ if (!fields.length || !ids.length)
417
+ return held;
418
+ for (const field of fields) {
419
+ const join = tables[joinTableOf(kayan, field)];
420
+ const rows = await client
421
+ .select()
422
+ .from(join)
423
+ .where(inArray(join.parent, ids))
424
+ .orderBy(asc(join.order));
425
+ for (const row of rows) {
426
+ held.get(String(row.parent))?.[field.name].push(row.target);
427
+ }
428
+ }
429
+ return held;
430
+ };
431
+ /** One row, with its sets read back off the join tables beside its columns. */
432
+ const withSets = async (client, name, row) => {
433
+ if (!manyFields(kayanByName.get(name)).length)
434
+ return row;
435
+ const held = await readSets(client, name, [row.id]);
436
+ return { ...row, ...held.get(String(row.id)) };
437
+ };
438
+ return {
439
+ capability: 'store',
440
+ type: 'drizzle',
441
+ sql: 'drizzle',
442
+ idKinds,
443
+ kinds,
444
+ rules,
445
+ db,
446
+ tables,
447
+ schemas,
448
+ enums,
449
+ connectionString,
450
+ commands: {
451
+ // Imported here, so an app that never migrates loads neither `node:child_process` nor `drizzle-kit`.
452
+ migrate: async ({ flags }) => (await import('./migrate.js')).run({ target: { db, tables, schemas, enums, connectionString, kayan: [...kayanByName.values()] }, flags }),
453
+ },
454
+ init(kayanList) {
455
+ const indexNames = [];
456
+ for (const kayan of kayanList) {
457
+ kayanByName.set(kayan.name, kayan);
458
+ // Under the kayan's own store's name, whatever the build calls it.
459
+ const override = kayan.targets?.[kayan.store];
460
+ if (override && kayan.schemaName !== undefined) {
461
+ throw new Error(`Kayan "${kayan.name}" writes schemaName beside a whole-table targets.drizzle, which names its own schema through its own pgSchema. Remove one of the two.`);
462
+ }
463
+ if (override && kayan.indexes.length) {
464
+ throw new Error(`Kayan "${kayan.name}" asks for indexes, its own or a unique field's, beside a whole-table targets.drizzle, which builds its own table and its indexes in its third argument. Build them there, and write indexes: () => [].`);
465
+ }
466
+ if (override) {
467
+ tables[kayan.name] = override;
468
+ continue;
469
+ }
470
+ const refused = kayan.schemaName === undefined ? undefined : refusedSchema(kayan.schemaName);
471
+ if (refused) {
472
+ throw new Error(`Kayan "${kayan.name}" asks for the schema ${JSON.stringify(kayan.schemaName)}, ${refused} Give the kayan another schemaName.`);
473
+ }
474
+ const built = Object.create(null);
475
+ const columns = Object.values(kayan.fields).flatMap(columnsOfField(kayan.store));
476
+ // `pgTable` keys on the field name, so a localized `title` and a plain `titleAr` would
477
+ // both ask for `title_ar` and one field would end with no column.
478
+ const asked = new Set();
479
+ const keyed = new Set();
480
+ for (const field of columns) {
481
+ if (asked.has(field.column)) {
482
+ throw new Error(`Kayan "${kayan.name}" field "${field.name}" asks for column "${field.column}", which another field of the same kayan already holds. Rename one of the two, or give one a dbName of its own.`);
483
+ }
484
+ // A `dbName` walks a plain field past the guard above while leaving its table key,
485
+ // which would overwrite the locale's entry.
486
+ if (keyed.has(field.name)) {
487
+ throw new Error(`Kayan "${kayan.name}" field "${field.name}" asks for the table key "${field.name}", which another field of the same kayan already holds. Rename one of the two; a dbName changes the column and not this key.`);
488
+ }
489
+ asked.add(field.column);
490
+ keyed.add(field.name);
491
+ }
492
+ for (const field of columns) {
493
+ // The field's entry under its kayan's own store is the whole column, rules skipped.
494
+ if (field.targets?.[kayan.store]) {
495
+ built[field.name] = field.targets[kayan.store];
496
+ continue;
497
+ }
498
+ // A kayan that keeps drafts makes a row before its rules hold, so no client column
499
+ // carries a length or a precision; the runner holds a publish to the rules instead.
500
+ const drafted = kayan.versions?.drafts && providerOf(field, 'create') === 'client';
501
+ // The project's own entry, then the kind's own, then the shipped table.
502
+ const build = implementationOf({ table: kinds, shipped: drizzleKinds, name: field.kind, own: field.kindTargets, type: 'drizzle' });
503
+ let column = modifiable(build(drafted ? { ...field, rules: { ...field.rules, maxLength: undefined, precision: undefined, scale: undefined } } : field, tables));
504
+ if (field.name === 'id') {
505
+ column = rules.primary({ column, value: undefined, field });
506
+ }
507
+ for (const [rule, apply] of Object.entries(rules)) {
508
+ // A draft is made before its rules hold, so no client column is `not null`; the runner
509
+ // holds a published row to `required` instead.
510
+ if (rule === 'required' && drafted)
511
+ continue;
512
+ if (rule in (field.rules ?? {})) {
513
+ column = apply({ column, value: field.rules[rule], field });
514
+ }
515
+ }
516
+ built[field.name] = column;
517
+ }
518
+ const indexed = columns.filter((field) => field.rules?.index);
519
+ // A unique rule's own index over one column is that column's UNIQUE, the constraint a unique
520
+ // field has always built, so a shared kayan's table is the one it was.
521
+ const ownUnique = (index) => uniqueRuleIndexes.has(index) && index.fields.length === 1;
522
+ for (const index of kayan.indexes.filter(ownUnique))
523
+ built[index.fields[0]] = built[index.fields[0]].unique();
524
+ // Named `<table>_<columns>_idx`, the same on every build whatever the kayan order; a
525
+ // name past 63 bytes is refused, not cut short as Postgres would cut it in silence.
526
+ // `nullsNotDistinct` is a unique constraint: drizzle's pg index builder has no NULLS NOT DISTINCT.
527
+ const compound = kayan.indexes.flatMap((index, at) => {
528
+ if (ownUnique(index))
529
+ return [];
530
+ const { fields, unique, nullsNotDistinct } = index;
531
+ const name = `${kayan.table}_${fields.map((field) => kayan.fields[field].column).join('_')}_${nullsNotDistinct ? 'unique' : 'idx'}`;
532
+ if (!isPlainIdentifier(name)) {
533
+ throw new Error(`Kayan "${kayan.name}" indexes[${at}] is named ${JSON.stringify(name)}, which is longer than the 63 bytes Postgres keeps of a name, or holds a character other than a letter, a digit or an underscore. Give the kayan or its fields a shorter dbName.`);
534
+ }
535
+ indexNames.push({ where: `Kayan "${kayan.name}" indexes[${at}]`, kayan: kayan.name, name });
536
+ return [{ name, fields, unique, nullsNotDistinct }];
537
+ });
538
+ tables[kayan.name] = tableIn(kayan.schemaName)(kayan.table, built, (table) => [
539
+ ...indexed.map((field) => index().on(table[field.name])),
540
+ ...compound.map(({ name, fields: [first, ...rest], unique, nullsNotDistinct }) => {
541
+ const on = [table[first], ...rest.map((field) => table[field])];
542
+ return nullsNotDistinct ? uniqueConstraint(name).on(...on).nullsNotDistinct() : (unique ? uniqueIndex : index)(name).on(...on);
543
+ }),
544
+ ]);
545
+ }
546
+ // In a pass of their own, so the collision check below sees every kayan's own key.
547
+ for (const kayan of kayanList) {
548
+ for (const field of manyFields(kayan)) {
549
+ const name = joinTableOf(kayan, field);
550
+ if (tables[name]) {
551
+ throw new Error(`Kayan "${kayan.name}" field "${field.name}" needs the join table "${name}", which is already the table of kayan "${name}". Rename one of the two, or give the kayan a dbName of its own.`);
552
+ }
553
+ // Each end takes the type of the id it points at. `cascade` on both: the row belongs to
554
+ // the pair. An upload's own builder answers `restrict`.
555
+ const reference = (build, column, target) => modifiable(build({ ...field, name: column, column: `${column}_id`, target, rules: { onDelete: 'cascade' } }, tables)).notNull();
556
+ // Named, so the check below holds them to the schema's one namespace.
557
+ const key = `${name}_pkey`;
558
+ const targetIndex = `${name}_target_id_index`;
559
+ // A name that is no plain identifier is refused below, so only the length is left here.
560
+ for (const [what, named] of [['key', key], ['index', targetIndex]]) {
561
+ if (isPlainIdentifier(name) && !isPlainIdentifier(named)) {
562
+ throw new Error(`Kayan "${kayan.name}" field "${field.name}" builds the join table "${name}", whose ${what} "${named}" is longer than the 63 bytes Postgres keeps of a name. Rename the field, or give the kayan a shorter dbName.`);
563
+ }
564
+ indexNames.push({ where: `Kayan "${kayan.name}" field "${field.name}" join table ${what}`, kayan: kayan.name, name: named });
565
+ }
566
+ tables[name] = tableIn(schemaOf(tables[kayan.name]))(name, {
567
+ parent: reference(kinds.relationship, 'parent', kayan),
568
+ target: reference(implementationOf({ table: kinds, shipped: drizzleKinds, name: field.kind, own: field.kindTargets, type: 'drizzle' }), 'target', field.target),
569
+ // The only order a set of ids has.
570
+ order: pgInteger('order').notNull(),
571
+ }, (table) => [
572
+ primaryKey({ name: key, columns: [table.parent, table.order] }),
573
+ index().on(table.parent),
574
+ index(targetIndex).on(table.target),
575
+ ]);
576
+ }
577
+ }
578
+ // The name must not be a table already built.
579
+ for (const kayan of kayanList) {
580
+ if (!kayan.versions)
581
+ continue;
582
+ const name = versionsTableOf(kayan);
583
+ if (tables[name]) {
584
+ throw new Error(`Kayan "${kayan.name}" keeps versions in the table "${name}", which is already the table of kayan "${name}". Rename one of the two, or give the kayan a dbName of its own.`);
585
+ }
586
+ // So a filter naming `parent` reaches the row it belongs to.
587
+ kayanByName.set(name, kayan.versions.kayan);
588
+ const moment = (column) => timestamp(column, { precision: 3, withTimezone: true, mode: 'date' }).notNull().defaultNow();
589
+ // The row it belongs to cascades: deleting the row deletes its versions, and a soft delete keeps them.
590
+ tables[name] = tableIn(schemaOf(tables[kayan.name]))(name, {
591
+ id: pgUuid('id').defaultRandom().primaryKey(),
592
+ parent: modifiable(kinds.relationship({ ...kayan.versions.kayan.fields.parent, rules: { onDelete: 'cascade' } }, tables)).notNull(),
593
+ version: jsonb('version').notNull(),
594
+ latest: pgBoolean('latest').notNull().default(false),
595
+ autosave: pgBoolean('autosave').notNull().default(false),
596
+ createdAt: moment('created_at'),
597
+ updatedAt: moment('updated_at'),
598
+ }, (table) => [index().on(table.parent), index().on(table.latest)]);
599
+ }
600
+ // Every name the push and the pull hand drizzle-kit as a glob passes here once, built.
601
+ for (const [key, table] of Object.entries(tables)) {
602
+ const name = getTableName(table);
603
+ if (typeof name !== 'string' || isPlainIdentifier(name))
604
+ continue;
605
+ const made = kayanList.flatMap((kayan) => [
606
+ ...manyFields(kayan)
607
+ .filter((field) => joinTableOf(kayan, field) === name)
608
+ .map((field) => [`Kayan "${kayan.name}" field "${field.name}" builds the join table`, 'Rename the field.']),
609
+ ...(kayan.versions && versionsTableOf(kayan) === name
610
+ ? [[`Kayan "${kayan.name}" builds the versions table`, 'Give the kayan a shorter dbName.']]
611
+ : []),
612
+ ])[0] ?? [`Kayan "${key}" whole-table targets.drizzle is the table`, 'Rename the table in targets.drizzle.'];
613
+ throw new Error(`${made[0]} ${JSON.stringify(name)}, which is not a plain SQL identifier: letters, digits and underscores, not starting with a digit, at most 63 bytes. ${made[1]}`);
614
+ }
615
+ // Postgres keeps tables and indexes under one name per schema, so a kayan index named as
616
+ // a table or another index would pass the build and fail the migration.
617
+ const named = new Map();
618
+ for (const table of Object.values(tables)) {
619
+ const name = getTableName(table);
620
+ if (typeof name === 'string')
621
+ named.set(`${schemaOf(table)}.${name}`, `the table "${name}"`);
622
+ }
623
+ for (const { where, kayan, name } of indexNames) {
624
+ const schema = schemaOf(tables[kayan]);
625
+ const taken = named.get(`${schema}.${name}`);
626
+ if (taken) {
627
+ throw new Error(`${where} is named "${name}", which is already ${taken} in the schema "${schema}", and Postgres keeps one name per schema for tables and indexes alike. Rename one of the two through a dbName.`);
628
+ }
629
+ named.set(`${schema}.${name}`, `the name of ${where.replace('Kayan ', 'kayan ')}`);
630
+ }
631
+ // Read off every table built, so a whole-table override in its own `pgSchema` is held to the same rule.
632
+ for (const [key, table] of Object.entries(tables)) {
633
+ if (typeof getTableName(table) !== 'string')
634
+ continue;
635
+ const name = schemaOf(table);
636
+ const refused = refusedSchema(name);
637
+ if (refused)
638
+ throw new Error(`Kayan "${key}" whole-table targets.drizzle is built in the schema ${JSON.stringify(name)}, ${refused} Build it in another schema.`);
639
+ if (!(name in schemas))
640
+ schemas[name] = name === 'public' ? null : pgSchema(name);
641
+ }
642
+ for (const name of Object.keys(schemas)) {
643
+ if (name !== 'public' && `${name}Schema` in tables) {
644
+ throw new Error(`The table key "${name}Schema" is the name the schema module exports the schema "${name}" by. Rename the kayan.`);
645
+ }
646
+ }
647
+ Object.assign(enums, enumsOf(tables));
648
+ },
649
+ async connect({ readOnly } = {}) {
650
+ // The `devPushMarker` row tells a pushed database from a migrated one, since `push` writes
651
+ // no history; without it the first `mithaq migrate` would recreate every table.
652
+ if (readOnly)
653
+ return;
654
+ if (!push) {
655
+ // Outside production nothing is checked: the dev push is that path.
656
+ if (process.env.NODE_ENV === 'production')
657
+ await (await import('./migrate.js')).bootMigrations({ db, prodMigrations });
658
+ return;
659
+ }
660
+ // Without the filter a table the kayan do not describe would be dropped. drizzle-kit reads
661
+ // an empty filter as none, so no tables means nothing to push.
662
+ const tablesFilter = Object.values(tables).map((table) => getTableName(table));
663
+ if (!tablesFilter.length)
664
+ return;
665
+ await refuseStrangers({ db, tables, by: 'push' });
666
+ // `drizzle-kit/api-postgres` imports every driver drizzle speaks, so webpack and Turbopack
667
+ // are told to leave the import to the runtime.
668
+ const { pushSchema } = await import(/* webpackIgnore: true */ 'drizzle-kit/api-postgres');
669
+ // `owned`: a column of a table of the language has the type, which makes it the language's own.
670
+ const listed = (values) => sql.join(values.map((value) => sql `${value}`), sql `, `);
671
+ const { rows } = await db.execute(sql `select nspname as schema, typname as name, array_agg(enumlabel::text order by enumsortorder) as labels,
672
+ exists (select 1 from pg_catalog.pg_attribute
673
+ join pg_catalog.pg_class on pg_class.oid = attrelid
674
+ join pg_catalog.pg_namespace as tables on tables.oid = relnamespace
675
+ where atttypid in (pg_type.oid, pg_type.typarray) and not attisdropped and tables.nspname in (${listed(Object.keys(schemas))}) and relname in (${listed(tablesFilter)})) as owned
676
+ from pg_catalog.pg_enum
677
+ join pg_catalog.pg_type on pg_type.oid = enumtypid
678
+ join pg_catalog.pg_namespace on pg_namespace.oid = typnamespace
679
+ where nspname in (${listed(Object.keys(schemas))})
680
+ group by nspname, typname, pg_type.oid`);
681
+ const { imports, filter } = pushInput({ tables, schemas, held: rows });
682
+ let planned;
683
+ try {
684
+ planned = await pushSchema(imports, db, filter);
685
+ }
686
+ catch (error) {
687
+ // A rename is a question drizzle-kit asks through hints the push cannot answer, and it throws.
688
+ if (error instanceof Error && error.message.includes('was called without a HintsHandler')) {
689
+ throw new Error("The push met a rename it cannot ask about, such as a field's column renamed. Run `mithaq migrate`, which asks, or drop the dev database's tables.", { cause: error });
690
+ }
691
+ throw error;
692
+ }
693
+ const { sqlStatements, hints } = planned;
694
+ // Refused outright: there is no terminal to answer in a test or a deploy, and a silent
695
+ // yes is how a column goes missing.
696
+ const dataLoss = hints.filter(({ statement }) => statement);
697
+ if (dataLoss.length) {
698
+ throw new Error(`Refusing to push the schema, because it would lose data:\n\n${dataLoss.map(({ hint }) => hint).join('\n')}`);
699
+ }
700
+ if (hints.length)
701
+ console.warn(`Schema push warnings:\n${hints.map(({ hint }) => hint).join('\n')}`);
702
+ // The drop is held back below, so recreating an enum would fail on a type that exists.
703
+ for (const statement of sqlStatements) {
704
+ const dropped = /^DROP TYPE (?:"(.+)"\.)?"(.+)";$/.exec(statement);
705
+ if (dropped && sqlStatements.some((one) => one.startsWith(`CREATE TYPE ${statement.slice(10, -1)} `))) {
706
+ throw new Error(`The push would recreate the enum "${dropped[1] ?? 'public'}"."${dropped[2]}", because a label of it was removed or reordered, and it never drops a type. Drop the dev table and the type with DROP TYPE "${dropped[1] ?? 'public'}"."${dropped[2]}", or keep the labels as they are. Nothing was written.`);
707
+ }
708
+ }
709
+ // drizzle-kit plans to drop every enum, sequence and schema it is not handed, another
710
+ // tool's included, so no `DROP TYPE`, `DROP SEQUENCE` or `DROP SCHEMA` runs.
711
+ const statements = sqlStatements.filter((statement) => !/^DROP (TYPE|SEQUENCE|SCHEMA) /.test(statement));
712
+ // drizzle-kit's own `apply` runs statement by statement with no transaction; Postgres
713
+ // DDL is transactional, so the push and its marker commit together or not at all.
714
+ // The history lock before any table lock, the order a boot takes them in, so the two never deadlock.
715
+ await db.transaction(async (tx) => {
716
+ await tx.execute(historyLock);
717
+ for (const statement of statements)
718
+ await tx.execute(sql.raw(statement));
719
+ // Deleted and written again, so the marker stays one row.
720
+ await recordMigration({ db: tx, hash: devPushMarker, createdAt: Date.now(), name: devPushMarker });
721
+ });
722
+ },
723
+ // Kept as drizzle's own callback: the runner passes `tx` down the list.
724
+ async transaction(run) {
725
+ return db.transaction((tx) => run(tx));
726
+ },
727
+ async insert({ tx, kayan: name, data: values }) {
728
+ const { columns, sets } = withoutSets(name, values);
729
+ let result;
730
+ try {
731
+ result = await tx.insert(tables[name]).values(flatten(name, columns)).returning();
732
+ }
733
+ catch (thrown) {
734
+ throw collision(thrown, name);
735
+ }
736
+ if (!Array.isArray(result))
737
+ throw new Error('The drizzle store expected insert to return rows.');
738
+ const [row] = result;
739
+ const held = expand(name, row);
740
+ // In the write's own transaction, so a set that cannot be written takes its row down.
741
+ await writeSets(tx, name, held.id, sets);
742
+ return withSets(tx, name, held);
743
+ },
744
+ async update({ tx, kayan: name, id, data: values, where }) {
745
+ const { columns, sets } = withoutSets(name, values);
746
+ const flat = flatten(name, columns);
747
+ const matched = idWhere(tx, tables, kayanByName, name, id, where);
748
+ let result;
749
+ try {
750
+ // drizzle refuses an empty `set`, so a write naming only sets locks the row instead.
751
+ result = Object.keys(flat).length
752
+ ? await tx.update(tables[name]).set(flat).where(matched).returning()
753
+ : await tx.select().from(tables[name]).where(matched).for('update');
754
+ }
755
+ catch (thrown) {
756
+ throw collision(thrown, name);
757
+ }
758
+ if (!Array.isArray(result))
759
+ throw new Error('The drizzle store expected update to return rows.');
760
+ const [row] = result;
761
+ // An update that matched nothing did not happen; an `undefined` row answer would hide it.
762
+ if (!row)
763
+ throw new Error(`No "${name}" row with id ${String(id)} to update.`);
764
+ const held = expand(name, row);
765
+ // After the row, so a `where` that admitted no row refuses before any set is touched.
766
+ await writeSets(tx, name, held.id, sets);
767
+ return withSets(tx, name, held);
768
+ },
769
+ async remove({ tx, kayan: name, id, where }) {
770
+ // Read before the delete: the join rows cascade with it.
771
+ const sets = await withSets(tx, name, { id });
772
+ let result;
773
+ try {
774
+ result = await tx
775
+ .delete(tables[name])
776
+ .where(idWhere(tx, tables, kayanByName, name, id, where))
777
+ .returning();
778
+ }
779
+ catch (thrown) {
780
+ throw referenced({ thrown, name, kayans: kayanByName, tables });
781
+ }
782
+ if (!Array.isArray(result))
783
+ throw new Error('The drizzle store expected delete to return rows.');
784
+ const [row] = result;
785
+ if (!row)
786
+ throw new Error(`No "${name}" row with id ${String(id)} to delete.`);
787
+ return { ...sets, ...expand(name, row) };
788
+ },
789
+ async find({ kayan: name, args: { limit, lock, page, sort, where }, tx }) {
790
+ const client = (tx ?? db);
791
+ const table = tables[name];
792
+ if (kayanByName.get(name)?.fields[sort.field]?.rules?.localized) {
793
+ throw refuseLocalized(name, sort.field, 'sort');
794
+ }
795
+ const condition = where ? whereOf(client, tables, kayanByName, name, where) : undefined;
796
+ const column = table[sort.field];
797
+ const id = table.id;
798
+ const direction = sort.direction === 'desc' ? desc : asc;
799
+ // Each clause only for a column the table has: a table override may lack `id`, and a
800
+ // clause from the absent column is a syntax error. The unique id makes page boundaries total.
801
+ const orderBy = [
802
+ ...(column ? [direction(column)] : []),
803
+ ...(id && sort.field !== 'id' ? [direction(id)] : []),
804
+ ];
805
+ const offset = limit > 0 ? (page - 1) * limit : 0;
806
+ // `$dynamic()` keeps each branch one type, so drizzle checks `orderBy` against the table.
807
+ const query = client.select().from(table).$dynamic().where(condition);
808
+ const locked = tx && lock === 'update' ? query.for('update') : query;
809
+ const ordered = locked.orderBy(...orderBy).offset(offset);
810
+ const docs = limit > 0 ? await ordered.limit(limit) : await ordered;
811
+ const rows = await client.select({ totalDocs: sqlCount() }).from(table).where(condition);
812
+ const held = docs.map((row) => expand(name, row));
813
+ const sets = await readSets(client, name, held.map((row) => row.id));
814
+ return {
815
+ docs: held.map((row) => ({ ...row, ...sets.get(String(row.id)) })),
816
+ totalDocs: Number(rows[0]?.totalDocs ?? 0),
817
+ };
818
+ },
819
+ async count({ kayan: name, args: { where }, tx }) {
820
+ const client = (tx ?? db);
821
+ // The relationship filters use subqueries, so there are no joins to deduplicate.
822
+ const rows = await client.select({ totalDocs: sqlCount() }).from(tables[name]).where(where ? whereOf(client, tables, kayanByName, name, where) : undefined);
823
+ return Number(rows[0]?.totalDocs ?? 0);
824
+ },
825
+ async destroy() {
826
+ // Only a pool this adapter opened: a `db` the developer passed is theirs to end.
827
+ if (ready)
828
+ return;
829
+ await opened.$client.end();
830
+ },
831
+ };
832
+ };