@nuxodin/item 0.6.4

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/item.d.ts +116 -0
  4. package/item.js +361 -0
  5. package/package.json +24 -0
  6. package/src/AsyncDataPoint.d.ts +52 -0
  7. package/src/AsyncDataPoint.js +280 -0
  8. package/src/Collection.experiment.js +63 -0
  9. package/src/Emitter.js +103 -0
  10. package/src/proxy.js +77 -0
  11. package/src/signal.js +95 -0
  12. package/tools/AsyncChild.js +44 -0
  13. package/tools/access.js +36 -0
  14. package/tools/beta/collectChanges.js +88 -0
  15. package/tools/beta/syncWith.js +22 -0
  16. package/tools/condition.js +65 -0
  17. package/tools/csvDataItem.js +139 -0
  18. package/tools/db/connect.js +27 -0
  19. package/tools/db/dialect/mysql.js +12 -0
  20. package/tools/db/dialect/pg.js +15 -0
  21. package/tools/db/dialect/sqlite.js +12 -0
  22. package/tools/db/shared.js +7 -0
  23. package/tools/db/sql.d.ts +24 -0
  24. package/tools/db/sql.js +83 -0
  25. package/tools/httpRouter.js +66 -0
  26. package/tools/jsonDataItem.js +29 -0
  27. package/tools/schema/build/input.js +120 -0
  28. package/tools/schema/db/indexeddb/from-db.js +29 -0
  29. package/tools/schema/db/indexeddb/to-db.js +115 -0
  30. package/tools/schema/db/mysql/from-db.js +16 -0
  31. package/tools/schema/db/mysql/from-field.js +101 -0
  32. package/tools/schema/db/mysql/to-db.js +275 -0
  33. package/tools/schema/db/mysql/to-field.js +60 -0
  34. package/tools/schema/db/pg/from-db.js +48 -0
  35. package/tools/schema/db/pg/from-field.js +71 -0
  36. package/tools/schema/db/pg/to-db.js +223 -0
  37. package/tools/schema/db/pg/to-field.js +52 -0
  38. package/tools/schema/db/shared/from-db.js +29 -0
  39. package/tools/schema/db/shared/sql.js +26 -0
  40. package/tools/schema/db/sqlite/from-db.js +39 -0
  41. package/tools/schema/db/sqlite/from-field.js +49 -0
  42. package/tools/schema/db/sqlite/to-db.js +127 -0
  43. package/tools/schema/db/sqlite/to-field.js +34 -0
  44. package/tools/schema/diff.js +90 -0
  45. package/tools/schema/dom/toInput.js +6 -0
  46. package/tools/schema/render/dom.js +26 -0
  47. package/tools/schema/render/html.js +40 -0
  48. package/tools/schema/utils.js +131 -0
  49. package/tools/schema/validator.js +92 -0
  50. package/tools/view/dom.js +96 -0
  51. package/tools/view/uhtml.js +40 -0
  52. package/tools/wsDenoRouter.js +108 -0
@@ -0,0 +1,60 @@
1
+ // mysql/to-field.js — JSON Schema property → MySQL column definition
2
+ import { quoteId, defaultLiteral } from "../shared/sql.js";
3
+ export { quoteId };
4
+
5
+ const B = 4; // bytes per char, worst-case utf8mb4
6
+
7
+ const formats = { date: "DATE", time: "TIME", "date-time": "DATETIME" };
8
+
9
+ export function toFieldDef(name, prop, { required = false } = {}) {
10
+ let type, unsigned = false;
11
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type];
12
+ const schemaType = types.find(t => t !== "null");
13
+ const nullable = !required || types.includes("null");
14
+
15
+ if (schemaType === "boolean") type = "TINYINT(1)";
16
+ else if (schemaType === "integer") ({ type, unsigned } = integerType(prop));
17
+ else if (schemaType === "number") type = prop.multipleOf ? "DECIMAL" : "DOUBLE";
18
+ else if (schemaType === "object" || schemaType === "array") type = "JSON";
19
+ else type = stringType(prop);
20
+
21
+ let sql = `${quoteId(name)} ${type}`;
22
+ if (unsigned) sql += " UNSIGNED";
23
+ sql += !nullable || prop["x-autoincrement"] ? " NOT NULL" : " NULL";
24
+ // TEXT/BLOB/JSON can't carry a literal default in MySQL; emitting one never round-trips → endless ALTERs.
25
+ if (prop.default != null && !/TEXT|BLOB|JSON/.test(type)) sql += ` DEFAULT ${defaultLiteral(prop)}`;
26
+ if (prop["x-autoincrement"]) sql += " AUTO_INCREMENT";
27
+ if (prop["$comment"]) {
28
+ sql += ` COMMENT '${prop["$comment"].replace(/'/g, "''")}'`;
29
+ }
30
+
31
+ return sql;
32
+ }
33
+
34
+ function integerType(prop) {
35
+ if (prop.minimum == null && prop.maximum == null) return { type: "INT", unsigned: !!prop["x-autoincrement"] };
36
+ const min = prop.minimum ?? -Infinity, max = prop.maximum ?? Infinity;
37
+ const unsigned = min >= 0;
38
+ if (unsigned) {
39
+ if (max <= 255) return { type: "TINYINT", unsigned };
40
+ if (max <= 65535) return { type: "SMALLINT", unsigned };
41
+ if (max <= 16777215) return { type: "MEDIUMINT", unsigned };
42
+ if (max <= 4294967295) return { type: "INT", unsigned };
43
+ return { type: "BIGINT", unsigned };
44
+ }
45
+ if (min >= -128 && max <= 127) return { type: "TINYINT", unsigned };
46
+ if (min >= -32768 && max <= 32767) return { type: "SMALLINT", unsigned };
47
+ if (min >= -8388608 && max <= 8388607) return { type: "MEDIUMINT", unsigned };
48
+ if (min >= -2147483648 && max <= 2147483647) return { type: "INT", unsigned };
49
+ return { type: "BIGINT", unsigned };
50
+ }
51
+
52
+ function stringType(prop) {
53
+ if (prop.format in formats) return formats[prop.format];
54
+ if (prop.contentEncoding === "base64") return prop.maxLength ? `VARBINARY(${prop.maxLength})` : "BLOB";
55
+ const maxLength = prop.maxLength ?? Math.floor(65535 / B), maxBytes = maxLength * B;
56
+ if (maxLength <= 255) return `VARCHAR(${maxLength})`;
57
+ if (maxBytes <= 65535) return "TEXT";
58
+ if (maxBytes <= 16777215) return "MEDIUMTEXT";
59
+ return "LONGTEXT";
60
+ }
@@ -0,0 +1,48 @@
1
+ // pg/from-db.js
2
+ import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
3
+ import { queryRows } from '../shared/sql.js'
4
+ import { schemaFromField } from './from-field.js'
5
+ import { quoteLit } from './to-field.js'
6
+
7
+ const dialect = {
8
+ tables: async (query) => (await queryRows(query, `
9
+ SELECT table_name
10
+ FROM information_schema.tables
11
+ WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
12
+ ORDER BY table_name
13
+ `)).map(r => r.table_name),
14
+
15
+ fields: async (query, table) => queryRows(query, `
16
+ WITH ix AS (
17
+ SELECT a.attname AS column_name,
18
+ bool_or(i.indisprimary) AS is_primary,
19
+ bool_or(i.indisunique AND NOT i.indisprimary AND array_length(i.indkey, 1) = 1) AS is_unique,
20
+ bool_or(NOT i.indisunique AND NOT i.indisprimary AND array_length(i.indkey, 1) = 1) AS is_index
21
+ FROM pg_class t
22
+ JOIN pg_namespace n ON n.oid = t.relnamespace
23
+ JOIN pg_index i ON i.indrelid = t.oid
24
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(i.indkey)
25
+ WHERE n.nspname = current_schema() AND t.relname = ${quoteLit(table)}
26
+ GROUP BY a.attname
27
+ )
28
+ SELECT c.column_name, c.data_type, c.udt_name, c.character_maximum_length,
29
+ c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default,
30
+ c.is_identity, d.description AS comment, ix.is_primary, ix.is_unique, ix.is_index
31
+ FROM information_schema.columns c
32
+ LEFT JOIN pg_namespace n ON n.nspname = c.table_schema
33
+ LEFT JOIN pg_class t ON t.relnamespace = n.oid AND t.relname = c.table_name
34
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attname = c.column_name
35
+ LEFT JOIN pg_description d ON d.objoid = t.oid AND d.objsubid = a.attnum
36
+ LEFT JOIN ix ON ix.column_name = c.column_name
37
+ WHERE c.table_schema = current_schema() AND c.table_name = ${quoteLit(table)}
38
+ ORDER BY c.ordinal_position
39
+ `),
40
+
41
+ fromField: (row) => ({
42
+ name: row.column_name,
43
+ prop: schemaFromField(row),
44
+ isRequired: row.is_nullable === 'NO',
45
+ }),
46
+ }
47
+
48
+ export const schemaFromDb = (query) => _schemaFromDb(query, dialect)
@@ -0,0 +1,71 @@
1
+ // pg/from-field.js
2
+
3
+ const intRanges = {
4
+ int2: [-32768, 32767],
5
+ smallint: [-32768, 32767],
6
+ int4: [-2147483648, 2147483647],
7
+ integer: [-2147483648, 2147483647],
8
+ int8: [-9007199254740991, 9007199254740991], // JS safe
9
+ bigint: [-9007199254740991, 9007199254740991],
10
+ }
11
+
12
+ const floats = new Set(['float4', 'float8', 'real', 'double precision'])
13
+ const strings = new Set(['varchar', 'bpchar'])
14
+
15
+ export function schemaFromField(row) {
16
+ const prop = {}
17
+ const type = String(row.udt_name || row.data_type || '').toLowerCase()
18
+ const dataType = String(row.data_type || '').toLowerCase()
19
+ const isAuto = row.is_identity === 'YES' || /^nextval\(/i.test(row.column_default ?? '')
20
+
21
+ if (type === 'bool' || dataType === 'boolean') prop.type = 'boolean'
22
+ else if (type in intRanges) {
23
+ const [min, max] = intRanges[type]
24
+ prop.type = 'integer'
25
+ if (!isAuto) { prop.minimum = min; prop.maximum = max }
26
+ } else if (floats.has(type) || floats.has(dataType)) prop.type = 'number'
27
+ else if (type === 'numeric' || dataType === 'numeric') {
28
+ prop.type = 'number'
29
+ if (row.numeric_scale > 0) prop.multipleOf = Math.pow(10, -row.numeric_scale)
30
+ } else if (strings.has(type)) {
31
+ prop.type = 'string'
32
+ if (row.character_maximum_length) prop.maxLength = row.character_maximum_length
33
+ if (type === 'bpchar' && row.character_maximum_length) prop.minLength = row.character_maximum_length
34
+ } else if (type === 'bytea') {
35
+ prop.type = 'string'
36
+ prop.contentEncoding = 'base64'
37
+ } else if (type === 'date') {
38
+ prop.type = 'string'
39
+ prop.format = 'date'
40
+ } else if (type.startsWith('time') || dataType.startsWith('time')) {
41
+ prop.type = 'string'
42
+ prop.format = 'time'
43
+ } else if (type.startsWith('timestamp') || dataType.startsWith('timestamp')) {
44
+ prop.type = 'string'
45
+ prop.format = 'date-time'
46
+ } else if (type === 'json' || type === 'jsonb') prop.type = 'object'
47
+ else prop.type = 'string'
48
+
49
+ const def = defaultValue(row.column_default, prop)
50
+ if (def !== undefined) prop.default = def
51
+ if (row.comment) prop['$comment'] = row.comment
52
+ if (row.is_primary) prop['x-index'] = 'primary'
53
+ else if (row.is_unique) prop['x-index'] = 'unique'
54
+ else if (row.is_index) prop['x-index'] = true
55
+ if (isAuto) prop['x-autoincrement'] = true
56
+ return prop
57
+ }
58
+
59
+ function defaultValue(value, prop) {
60
+ if (value == null || /^nextval\(/i.test(value) || /\b(current_|now\()/i.test(value)) return undefined
61
+ let v = String(value).replace(/::[\w\s.[\]"]+$/i, '')
62
+ if (/^'.*'$/.test(v)) v = v.slice(1, -1).replace(/''/g, "'")
63
+ if (prop.type === 'boolean') return /^(true|t|1)$/i.test(v)
64
+ if (prop.type === 'integer') return numberOr(v, Number.parseInt(v, 10))
65
+ if (prop.type === 'number') return numberOr(v, Number(v))
66
+ return v
67
+ }
68
+
69
+ function numberOr(fallback, value) {
70
+ return Number.isNaN(value) ? fallback : value
71
+ }
@@ -0,0 +1,223 @@
1
+ // pg/to-db.js
2
+ import { schemaFromDb } from './from-db.js'
3
+ import { queryRows } from '../shared/sql.js'
4
+ import { defaultLiteral, quoteId, quoteLit, toFieldDef, typeSql } from './to-field.js'
5
+ import { schemaDiff } from '../../diff.js'
6
+
7
+ function tableData(tableSchema) {
8
+ const row = tableSchema.additionalProperties ?? {}
9
+ const fields = Object.entries(row.properties ?? {})
10
+ const required = new Set(row.required ?? [])
11
+ return { fields, required }
12
+ }
13
+
14
+ function primaryFields(fields) {
15
+ return fields.filter(([, f]) => f['x-index'] === 'primary').map(([n]) => n)
16
+ }
17
+
18
+ const columnProps = new Set([
19
+ 'type', 'format', 'contentEncoding', 'maxLength', 'minLength',
20
+ 'minimum', 'maximum', 'multipleOf',
21
+ ])
22
+
23
+ function hasOwn(obj, key) {
24
+ return Object.prototype.hasOwnProperty.call(obj, key)
25
+ }
26
+
27
+ function setEqual(a, b) {
28
+ if (a.size !== b.size) return false
29
+ for (const v of a) if (!b.has(v)) return false
30
+ return true
31
+ }
32
+
33
+ function fieldChanged(diffs, table, field, prop) {
34
+ return diffs.some(d => {
35
+ const [a, b, c, e, f] = d.path
36
+ return a === 'properties' && b === table && c === 'additionalProperties' && e === 'properties' && f === field &&
37
+ (columnProps.has(d.property) || d.property === 'default' && hasOwn(prop, 'default') || d.property === 'x-autoincrement')
38
+ })
39
+ }
40
+
41
+ function preserveDefault(prop, currProp) {
42
+ return hasOwn(prop, 'default') || !hasOwn(currProp, 'default')
43
+ ? prop
44
+ : { ...prop, default: currProp.default }
45
+ }
46
+
47
+ function secondaryIndexKind(table, name, prop) {
48
+ if (prop['x-index'] === 'unique') return 'UNIQUE INDEX'
49
+ if (prop['x-index'] === true) return 'INDEX'
50
+ if (prop['x-index'] === 'fulltext')
51
+ console.warn(`Skip FULLTEXT INDEX ${table}.${name}: PostgreSQL fulltext requires an expression index`)
52
+ return null
53
+ }
54
+
55
+ function indexName(table, name) {
56
+ return `idx_${table}_${name}`
57
+ }
58
+
59
+ function createIndex(table, name, kind) {
60
+ return `CREATE ${kind} ${quoteId(indexName(table, name))} ON ${quoteId(table)} (${quoteId(name)});`
61
+ }
62
+
63
+ async function tableIndexes(query, table) {
64
+ const primaryRows = await queryRows(query, `
65
+ SELECT con.conname, array_agg(a.attname ORDER BY u.ord) AS columns
66
+ FROM pg_constraint con
67
+ JOIN pg_class t ON t.oid = con.conrelid
68
+ JOIN pg_namespace n ON n.oid = t.relnamespace
69
+ JOIN unnest(con.conkey) WITH ORDINALITY AS u(attnum, ord) ON true
70
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = u.attnum
71
+ WHERE con.contype = 'p' AND n.nspname = current_schema() AND t.relname = ${quoteLit(table)}
72
+ GROUP BY con.conname
73
+ `)
74
+ const secondary = await queryRows(query, `
75
+ SELECT ci.relname AS key, a.attname AS column, i.indisunique AS unique
76
+ FROM pg_index i
77
+ JOIN pg_class t ON t.oid = i.indrelid
78
+ JOIN pg_namespace n ON n.oid = t.relnamespace
79
+ JOIN pg_class ci ON ci.oid = i.indexrelid
80
+ JOIN unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
81
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
82
+ WHERE NOT i.indisprimary
83
+ AND array_length(i.indkey, 1) = 1
84
+ AND n.nspname = current_schema()
85
+ AND t.relname = ${quoteLit(table)}
86
+ `)
87
+ const primary = primaryRows[0]
88
+ return { primaryName: primary?.conname, primary: primary?.columns ?? [], secondary }
89
+ }
90
+
91
+ function commentStmt(table, name, comment) {
92
+ return `COMMENT ON COLUMN ${quoteId(table)}.${quoteId(name)} IS ${comment ? quoteLit(comment) : 'NULL'};`
93
+ }
94
+
95
+ function alterFieldStmts(table, name, prop, currProp, required, currRequired, changed) {
96
+ const stmts = [], tableId = quoteId(table), colId = quoteId(name)
97
+ if (currProp['x-autoincrement'] && !prop['x-autoincrement'])
98
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} DROP IDENTITY IF EXISTS;`)
99
+ if (changed && typeSql(prop) !== typeSql(currProp))
100
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} TYPE ${typeSql(prop)} USING ${colId}::${typeSql(prop)};`)
101
+ if (prop['x-autoincrement'] && !currProp['x-autoincrement'] && hasOwn(currProp, 'default'))
102
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} DROP DEFAULT;`)
103
+ if (prop['x-autoincrement'] && !currProp['x-autoincrement'])
104
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} ADD GENERATED BY DEFAULT AS IDENTITY;`)
105
+ if (hasOwn(prop, 'default') && !prop['x-autoincrement'])
106
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} SET DEFAULT ${defaultLiteral(prop)};`)
107
+ if (required !== currRequired)
108
+ stmts.push(`ALTER TABLE ${tableId} ALTER COLUMN ${colId} ${required ? 'SET' : 'DROP'} NOT NULL;`)
109
+ if (prop['$comment'] !== currProp['$comment'])
110
+ stmts.push(commentStmt(table, name, prop['$comment']))
111
+ return stmts
112
+ }
113
+
114
+ export async function schemaToDb(schema, query, { force = false, patch = false } = {}) {
115
+ const current = await schemaFromDb(query)
116
+ const diffs = schemaDiff(schema, current)
117
+
118
+ if (!patch) {
119
+ const destructive = diffs.filter(d => d.destructive === true)
120
+ if (destructive.length && !force) {
121
+ const msgs = destructive.map(d => ` ${d.path.join('.')} - ${d.msg}`).join('\n')
122
+ throw new Error(`Destructive schema changes detected (use force to apply):\n${msgs}`)
123
+ }
124
+ }
125
+
126
+ const stmts = []
127
+ const nextTables = Object.keys(schema.properties ?? {})
128
+ const currTables = Object.keys(current.properties ?? {})
129
+
130
+ for (const table of nextTables) {
131
+ const tableId = quoteId(table)
132
+ const { fields, required } = tableData(schema.properties[table])
133
+ const primaries = primaryFields(fields)
134
+ for (const p of primaries) required.add(p)
135
+
136
+ if (!currTables.includes(table)) {
137
+ const cols = fields.map(([n, f]) =>
138
+ ' ' + toFieldDef(n, f, { required: required.has(n) })
139
+ ).join(',\n')
140
+ const pk = primaries.length
141
+ ? `,\n PRIMARY KEY (${primaries.map(quoteId).join(', ')})`
142
+ : ''
143
+ const primarySet = new Set(primaries)
144
+ stmts.push(`CREATE TABLE ${tableId} (\n${cols}${pk}\n);`)
145
+ for (const [n, f] of fields) if (f['$comment']) stmts.push(commentStmt(table, n, f['$comment']))
146
+ for (const [n, f] of fields) {
147
+ if (primarySet.has(n)) continue
148
+ const kind = secondaryIndexKind(table, n, f)
149
+ if (kind) stmts.push(createIndex(table, n, kind))
150
+ }
151
+ } else {
152
+ const { fields: currFieldEntries, required: currRequired } = tableData(current.properties[table])
153
+ const currFields = new Map(currFieldEntries)
154
+ const nextFieldNames = new Set(fields.map(([n]) => n))
155
+ const indexStmts = await indexStatements(query, table, fields, currFieldEntries, { patch })
156
+ stmts.push(...indexStmts.before)
157
+ for (const [name, prop] of fields) {
158
+ const isRequired = required.has(name)
159
+ const currProp = currFields.get(name)
160
+ if (!currProp) {
161
+ stmts.push(`ALTER TABLE ${tableId} ADD COLUMN ${toFieldDef(name, prop, { required: isRequired })};`)
162
+ if (prop['$comment']) stmts.push(commentStmt(table, name, prop['$comment']))
163
+ continue
164
+ }
165
+ const propWithDefault = preserveDefault(prop, currProp)
166
+ const changed = fieldChanged(diffs, table, name, prop) || isRequired !== currRequired.has(name) || propWithDefault['$comment'] !== currProp['$comment']
167
+ if (changed) stmts.push(...alterFieldStmts(table, name, propWithDefault, currProp, isRequired, currRequired.has(name), changed))
168
+ }
169
+ if (!patch) {
170
+ for (const name of currFields.keys()) {
171
+ if (!nextFieldNames.has(name))
172
+ stmts.push(`ALTER TABLE ${tableId} DROP COLUMN ${quoteId(name)};`)
173
+ }
174
+ }
175
+ stmts.push(...indexStmts.after)
176
+ }
177
+ }
178
+
179
+ if (!patch) {
180
+ for (const table of currTables) {
181
+ if (!nextTables.includes(table)) stmts.push(`DROP TABLE ${quoteId(table)};`)
182
+ }
183
+ }
184
+
185
+ for (const stmt of stmts) {
186
+ console.log(stmt)
187
+ await query(stmt)
188
+ }
189
+ return { diffs, executed: stmts }
190
+ }
191
+
192
+ async function indexStatements(query, table, nextFields, currFields, { patch = false } = {}) {
193
+ const before = [], after = [], tableId = quoteId(table)
194
+ const indexes = await tableIndexes(query, table)
195
+ const currPrimary = new Set(indexes.primary.length ? indexes.primary : primaryFields(currFields))
196
+ const nextPrimary = new Set(primaryFields(nextFields))
197
+ const primaryChanged = !setEqual(currPrimary, nextPrimary) && (!patch || nextPrimary.size)
198
+
199
+ if (primaryChanged) {
200
+ if (indexes.primaryName) before.push(`ALTER TABLE ${tableId} DROP CONSTRAINT ${quoteId(indexes.primaryName)};`)
201
+ if (nextPrimary.size) after.push(`ALTER TABLE ${tableId} ADD PRIMARY KEY (${[...nextPrimary].map(quoteId).join(', ')});`)
202
+ }
203
+
204
+ for (const [name, prop] of nextFields) {
205
+ if (nextPrimary.has(name)) continue
206
+ const nextKind = secondaryIndexKind(table, name, prop)
207
+ const curr = indexes.secondary.find(i => i.column === name)
208
+ const currKind = curr ? curr.unique ? 'UNIQUE INDEX' : 'INDEX' : null
209
+ const changed = curr && currKind !== nextKind
210
+ if (patch && !nextKind) continue
211
+ if (changed) before.push(`DROP INDEX ${quoteId(curr.key)};`)
212
+ if (nextKind && (!curr || changed)) after.push(createIndex(table, name, nextKind))
213
+ }
214
+
215
+ if (!patch) {
216
+ for (const curr of indexes.secondary) {
217
+ const wanted = nextFields.find(([name, prop]) => name === curr.column && secondaryIndexKind(table, name, prop))
218
+ if (!wanted) before.push(`DROP INDEX ${quoteId(curr.key)};`)
219
+ }
220
+ }
221
+
222
+ return { before, after }
223
+ }
@@ -0,0 +1,52 @@
1
+ // pg/to-field.js - JSON Schema property -> PostgreSQL column definition
2
+ import { schemaType } from '../shared/sql.js'
3
+
4
+ const formats = { date: 'DATE', time: 'TIME', 'date-time': 'TIMESTAMP' }
5
+ const maxVarchar = 10485760
6
+
7
+ export function quoteId(name) {
8
+ return `"${String(name).replaceAll('"', '""')}"`
9
+ }
10
+
11
+ export function quoteLit(value) {
12
+ return `'${String(value).replace(/'/g, "''")}'`
13
+ }
14
+
15
+ export function typeSql(prop) {
16
+ const t = schemaType(prop)
17
+ if (t === 'boolean') return 'BOOLEAN'
18
+ if (t === 'integer') return integerType(prop)
19
+ if (t === 'number') return prop.multipleOf ? 'NUMERIC' : 'DOUBLE PRECISION'
20
+ if (t === 'object' || t === 'array') return 'JSONB'
21
+ if (prop.format in formats) return formats[prop.format]
22
+ if (prop.contentEncoding === 'base64') return 'BYTEA'
23
+ return prop.maxLength && prop.maxLength <= maxVarchar ? `VARCHAR(${prop.maxLength})` : 'TEXT'
24
+ }
25
+
26
+ export function defaultLiteral(prop) {
27
+ const t = schemaType(prop)
28
+ if (t === 'boolean') return prop.default ? 'TRUE' : 'FALSE'
29
+ if (t === 'integer' || t === 'number') return Number(prop.default)
30
+ if (typeof prop.default === 'boolean') return "''"
31
+ if (t === 'object' || t === 'array') return `${quoteLit(JSON.stringify(prop.default))}::jsonb`
32
+ return quoteLit(prop.default)
33
+ }
34
+
35
+ export function toFieldDef(name, prop, { required = false } = {}) {
36
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type]
37
+ const nullable = !required || types.includes('null')
38
+ let sql = `${quoteId(name)} ${typeSql(prop)}`
39
+ if (prop['x-autoincrement']) sql += ' GENERATED BY DEFAULT AS IDENTITY'
40
+ sql += !nullable || prop['x-autoincrement'] ? ' NOT NULL' : ' NULL'
41
+ if (prop.default != null && !prop['x-autoincrement']) sql += ` DEFAULT ${defaultLiteral(prop)}`
42
+ return sql
43
+ }
44
+
45
+ function integerType(prop) {
46
+ if (prop['x-autoincrement']) return 'INTEGER'
47
+ if (prop.minimum == null && prop.maximum == null) return 'INTEGER'
48
+ const min = prop.minimum ?? -Infinity, max = prop.maximum ?? Infinity
49
+ if (min >= -32768 && max <= 32767) return 'SMALLINT'
50
+ if (min >= -2147483648 && max <= 2147483647) return 'INTEGER'
51
+ return 'BIGINT'
52
+ }
@@ -0,0 +1,29 @@
1
+ // shared/from-db.js
2
+
3
+ export function schemaFromTable(fields, fromField) {
4
+ const properties = {}, required = [];
5
+ for (const f of fields) {
6
+ const { name, prop, isRequired } = fromField(f);
7
+ properties[name] = prop;
8
+ if (isRequired) required.push(name);
9
+ }
10
+ return {
11
+ additionalProperties: {
12
+ properties,
13
+ ...(required.length && { required }),
14
+ },
15
+ };
16
+ }
17
+
18
+ export async function schemaFromDb(db, dialect) {
19
+ const tables = await dialect.tables(db);
20
+ const entries = await Promise.all(
21
+ tables.map(
22
+ async (t) => [
23
+ t,
24
+ schemaFromTable(await dialect.fields(db, t), dialect.fromField),
25
+ ],
26
+ ),
27
+ );
28
+ return { properties: Object.fromEntries(entries) };
29
+ }
@@ -0,0 +1,26 @@
1
+ // shared/sql.js — helpers shared by SQL dialects
2
+
3
+ export function quoteId(name) {
4
+ return `\`${String(name).replaceAll('`', '``')}\``;
5
+ }
6
+
7
+ export async function queryRows(query, sql) {
8
+ const res = await query(sql);
9
+ return Array.isArray(res) ? res : res.rows ?? [];
10
+ }
11
+
12
+ /** First non-null JSON-Schema type: 'integer' | ['string','null'] → 'string'. */
13
+ export function schemaType(prop) {
14
+ return Array.isArray(prop.type) ? prop.type.find(t => t !== 'null') : prop.type;
15
+ }
16
+
17
+ const numericTypes = new Set(['boolean', 'integer', 'number']);
18
+
19
+ /** JSON-Schema default → SQL literal: numeric columns get an unquoted number
20
+ * (boolean false→0, true→1), string columns a quoted literal ('' for a boolean).
21
+ * Used by mysql/sqlite; dialects with native literals can keep their own renderer. */
22
+ export function defaultLiteral(prop) {
23
+ if (numericTypes.has(schemaType(prop))) return Number(prop.default);
24
+ if (typeof prop.default === 'boolean') return "''";
25
+ return `'${String(prop.default).replace(/'/g, "''")}'`;
26
+ }
@@ -0,0 +1,39 @@
1
+ // sqlite/from-db.js
2
+ import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
3
+ import { quoteId } from '../shared/sql.js'
4
+ import { schemaFromField } from './from-field.js'
5
+
6
+ /** Single-column, non-constraint indexes: [{ name, column, unique }]. */
7
+ export async function singleColumnIndexes(query, table) {
8
+ const out = []
9
+ for (const row of await query(`PRAGMA index_list(${quoteId(table)})`)) {
10
+ if (row.origin && row.origin !== 'c') continue
11
+ const cols = await query(`PRAGMA index_info(${quoteId(row.name)})`)
12
+ if (cols.length === 1) out.push({ name: row.name, column: cols[0].name, unique: !!row.unique })
13
+ }
14
+ return out
15
+ }
16
+
17
+ async function indexes(query, table) {
18
+ const map = new Map()
19
+ for (const { column, unique } of await singleColumnIndexes(query, table))
20
+ map.set(column, unique ? 'unique' : true)
21
+ return map
22
+ }
23
+
24
+ const dialect = {
25
+ tables: async (query) => (await query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")).map(r => r.name),
26
+ fields: async (query, table) => {
27
+ const ix = await indexes(query, table)
28
+ return (await query(`PRAGMA table_info(${quoteId(table)})`)).map(row =>
29
+ row.pk || !ix.has(row.name) ? row : { ...row, index: ix.get(row.name) }
30
+ )
31
+ },
32
+ fromField: (row) => ({
33
+ name: row.name,
34
+ prop: schemaFromField(row),
35
+ isRequired: row.notnull === 1 && !row.pk,
36
+ }),
37
+ }
38
+
39
+ export const schemaFromDb = (query) => _schemaFromDb(query, dialect)
@@ -0,0 +1,49 @@
1
+ // sqlite/schema-from-field.js
2
+ // PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
3
+ // SQLite lengths are in characters — no byte conversion needed
4
+
5
+ const intRanges = {
6
+ tinyint: { signed: [-128, 127] },
7
+ smallint: { signed: [-32768, 32767] },
8
+ mediumint: { signed: [-8388608, 8388607] },
9
+ int: { signed: [-2147483648, 2147483647] },
10
+ integer: { signed: [-2147483648, 2147483647] },
11
+ bigint: { signed: [-9007199254740991, 9007199254740991] }, // JS safe
12
+ }
13
+
14
+ export function schemaFromField(row) {
15
+ const prop = {}
16
+ const raw = (row.type ?? '').trim()
17
+ const m = raw.match(/^(\w+)\s*(?:\(([^)]+)\))?/)
18
+ const type = m?.[1]?.toLowerCase() ?? 'text'
19
+ const len = m?.[2] ? parseInt(m[2]) : null
20
+
21
+ if (type in intRanges) {
22
+ const [min, max] = intRanges[type].signed
23
+ prop.type = 'integer'; prop.minimum = min; prop.maximum = max
24
+
25
+ } else if (type === 'boolean' || type === 'bool') { prop.type = 'boolean'
26
+ } else if (type === 'real' || type === 'float' || type === 'double') { prop.type = 'number'
27
+ } else if (type === 'numeric' || type === 'decimal') { prop.type = 'number'
28
+
29
+ } else if (type === 'varchar' || type === 'nvarchar') {
30
+ prop.type = 'string'; if (len) prop.maxLength = len
31
+
32
+ } else if (type === 'char' || type === 'nchar') {
33
+ prop.type = 'string'; if (len) { prop.minLength = len; prop.maxLength = len }
34
+
35
+ } else if (type === 'blob') { prop.type = 'string'; prop.contentEncoding = 'base64'
36
+ } else if (type === 'date') { prop.type = 'string'; prop.format = 'date'
37
+ } else if (type === 'time') { prop.type = 'string'; prop.format = 'time'
38
+ } else if (type === 'datetime' || type === 'timestamp') { prop.type = 'string'; prop.format = 'date-time'
39
+ } else if (type === 'json') { prop.type = 'object'
40
+ } else {
41
+ prop.type = 'string'
42
+ }
43
+
44
+ if (row.dflt_value != null) prop.default = row.dflt_value
45
+ if (row.pk) prop['x-index'] = 'primary'
46
+ else if (row.index) prop['x-index'] = row.index
47
+
48
+ return prop
49
+ }