@primate/mysql 0.5.0 → 0.7.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.
@@ -20,6 +20,8 @@ type ColumnTypes = Validate<{
20
20
  "TIME": string;
21
21
  TINYINT: number;
22
22
  "TINYINT UNSIGNED": number;
23
+ JSON: string;
24
+ "BINARY(16)": Uint8Array<ArrayBuffer>;
23
25
  }>;
24
26
  export type { ColumnTypes as default };
25
27
  //# sourceMappingURL=ColumnTypes.d.ts.map
@@ -1,28 +1,35 @@
1
- import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
1
+ import type { As, DataDict, ExtractPlaceholders, Schema, Sort, SQLDB, With, CheckPlaceholders } from "@primate/core/db";
2
2
  import type { Dict } from "@rcompat/type";
3
- import type { StoreSchema } from "pema";
4
- declare const schema: import("pema").ObjectType<{
5
- database: import("pema").StringType;
6
- host: import("pema").DefaultType<import("pema").StringType, "localhost">;
7
- password: import("pema").OptionalType<import("pema").StringType>;
8
- port: import("pema").DefaultType<import("pema").UintType<"u32">, 3306>;
9
- username: import("pema").OptionalType<import("pema").StringType>;
3
+ import type { Pool } from "mysql2/promise";
4
+ import type { ObjectType, Parsed } from "pema";
5
+ declare const schema: ObjectType<import("pema").NormalizeSchemaObject<{
6
+ readonly host: import("pema").DefaultType<import("pema").StringType, "localhost">;
7
+ readonly port: import("pema").DefaultType<import("pema").UintType<"u32">, 3306>;
8
+ readonly database: import("pema").StringType;
9
+ readonly password: import("pema").OptionalType<import("pema").StringType>;
10
+ readonly username: import("pema").OptionalType<import("pema").StringType>;
11
+ readonly jsonStrings: import("pema").DefaultType<import("pema").BooleanType, true>;
12
+ }>, {
13
+ host: string;
14
+ port: number;
15
+ database: string;
16
+ password: string | undefined;
17
+ username: string | undefined;
18
+ jsonStrings: boolean;
10
19
  }>;
11
- export default class MySQL implements DB {
20
+ export default class MySQL implements SQLDB<Pool> {
12
21
  #private;
13
22
  static config: typeof schema.input;
14
23
  constructor(config?: typeof schema.input, options?: {
15
24
  debug?: boolean;
16
25
  });
26
+ get client(): Pool;
17
27
  get explain(): Dict<{
18
28
  query: string;
19
29
  plans: string[];
20
30
  }>;
21
31
  close(): Promise<void>;
22
- get schema(): {
23
- create: (as: As, store: StoreSchema) => Promise<void>;
24
- delete: (table: string) => Promise<void>;
25
- };
32
+ get schema(): Schema;
26
33
  create<O extends Dict>(as: As, record: Dict): Promise<O>;
27
34
  read(as: As, args: {
28
35
  count: true;
@@ -43,6 +50,13 @@ export default class MySQL implements DB {
43
50
  delete(as: As, args: {
44
51
  where: DataDict;
45
52
  }): Promise<number>;
53
+ sql<Q extends string, I extends ObjectType<{
54
+ [K in ExtractPlaceholders<Q>]: Parsed<unknown>;
55
+ }> | undefined, O extends Parsed<unknown> | undefined>(options: {
56
+ input?: I extends ObjectType<any> ? CheckPlaceholders<I, Q> : I;
57
+ query: Q;
58
+ output?: O;
59
+ }): (args: I extends ObjectType<any> ? I["infer"] : void) => Promise<O extends Parsed<unknown> ? O["infer"] : void>;
46
60
  }
47
61
  export {};
48
62
  //# sourceMappingURL=MySQL.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import typemap from "#typemap";
2
- import common from "@primate/core/db";
3
- import E from "@primate/core/db/error";
2
+ import base from "@primate/core/db";
3
+ import E from "@primate/core/db/errors";
4
4
  import sql from "@primate/core/db/sql";
5
5
  import assert from "@rcompat/assert";
6
6
  import is from "@rcompat/is";
@@ -15,14 +15,14 @@ function is_bigint_key(k) {
15
15
  return k === "u64" || k === "u128" || k === "i64" || k === "i128";
16
16
  }
17
17
  function order_by(types, sort, alias) {
18
- if (sort === undefined)
18
+ if (is.undefined(sort))
19
19
  return "";
20
20
  const entries = Object.entries(sort);
21
21
  if (entries.length === 0)
22
22
  return "";
23
23
  const parts = entries.map(([k, dir]) => {
24
24
  const quoted = sql.quote(k);
25
- const base = alias ? `${alias}.${quoted}` : quoted;
25
+ const base = is.defined(alias) ? `${alias}.${quoted}` : quoted;
26
26
  const expression = is_bigint_key(types[k]) ? bigint_cast(base) : base;
27
27
  return `${expression} ${dir.toLowerCase() === "desc" ? "DESC" : "ASC"}`;
28
28
  });
@@ -42,12 +42,43 @@ function unbind_value(key, value) {
42
42
  function unbind(types, row) {
43
43
  return sql.unbind(types, row, unbind_value);
44
44
  }
45
+ function columns_to_types(data_t, column_t) {
46
+ const unsigned = column_t.includes("unsigned");
47
+ if (data_t === "varchar")
48
+ return ["string", "url"];
49
+ if (data_t === "text")
50
+ return ["string", "url", "time"];
51
+ if (data_t === "binary" && column_t.includes("(16)"))
52
+ return base.UUID_TYPES;
53
+ if (data_t === "smallint")
54
+ return unsigned ? ["u16"] : ["i16"];
55
+ if (data_t === "int")
56
+ return unsigned ? ["u32"] : ["i32"];
57
+ if (data_t === "bigint")
58
+ return unsigned ? ["u64"] : ["i64"];
59
+ if (data_t === "decimal")
60
+ return ["u128", "i128"];
61
+ if (data_t === "double")
62
+ return ["f32", "f64"];
63
+ if (data_t === "datetime")
64
+ return ["datetime"];
65
+ if (data_t === "tinyint" && column_t === "tinyint(1)")
66
+ return ["boolean"];
67
+ if (data_t === "tinyint")
68
+ return unsigned ? ["u8"] : ["i8", "boolean"];
69
+ if (data_t === "blob")
70
+ return ["blob"];
71
+ if (data_t === "json")
72
+ return ["json"];
73
+ return [];
74
+ }
45
75
  const schema = p({
46
- database: p.string,
47
76
  host: p.string.default("localhost"),
48
- password: p.string.optional(),
49
77
  port: p.uint.port().default(3306),
78
+ database: p.string,
79
+ password: p.string.optional(),
50
80
  username: p.string.optional(),
81
+ jsonStrings: p.boolean.default(true),
51
82
  });
52
83
  export default class MySQL {
53
84
  static config;
@@ -66,12 +97,16 @@ export default class MySQL {
66
97
  namedPlaceholders: true,
67
98
  bigNumberStrings: true,
68
99
  supportBigNumbers: true,
100
+ jsonStrings: parsed.jsonStrings,
69
101
  });
70
102
  this.#debug = options?.debug ?? false;
71
103
  }
72
104
  get #db() {
73
105
  return this.#client ??= this.#factory();
74
106
  }
107
+ get client() {
108
+ return this.#db;
109
+ }
75
110
  async #sql(query, params) {
76
111
  const [rows] = await this.#db.query(query, params);
77
112
  return rows;
@@ -94,26 +129,63 @@ export default class MySQL {
94
129
  }
95
130
  get schema() {
96
131
  return {
97
- create: async (as, store) => {
132
+ create: async (table, pk, types) => {
98
133
  const columns = [];
99
- for (const [key, value] of Object.entries(store)) {
100
- const type = get_column(value.datatype);
101
- if (key === as.pk) {
102
- const is_int = common.INT_TYPES.includes(value.datatype);
103
- const auto = as.generate_pk && is_int ? " AUTO_INCREMENT" : "";
134
+ for (const [key, value] of Object.entries(types)) {
135
+ const type = get_column(value);
136
+ if (key === pk.name) {
137
+ const is_int = base.INT_TYPES.includes(value);
138
+ const auto = pk.generate && is_int ? " AUTO_INCREMENT" : "";
104
139
  // 36 is UUID length
105
- const pk_type = value.datatype === "string" ? "VARCHAR(36)" : type;
140
+ const pk_type = value === "string" ? "VARCHAR(36)" : type;
106
141
  columns.push(`${sql.quote(key)} ${pk_type} PRIMARY KEY${auto}`);
107
142
  }
108
143
  else {
109
144
  columns.push(`${sql.quote(key)} ${type}`);
110
145
  }
111
146
  }
112
- await this.#sql(Q `CREATE TABLE IF NOT EXISTS ${as.table} (${columns})`);
147
+ await this.#sql(Q `CREATE TABLE IF NOT EXISTS ${table} (${columns})`);
113
148
  },
114
149
  delete: async (table) => {
115
150
  await this.#sql(Q `DROP TABLE IF EXISTS ${table}`);
116
151
  },
152
+ introspect: async (table) => {
153
+ const rows = await this.#sql(`
154
+ SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE
155
+ FROM information_schema.COLUMNS
156
+ WHERE TABLE_SCHEMA = DATABASE()
157
+ AND TABLE_NAME = :name
158
+ `, { name: table });
159
+ if (rows.length === 0)
160
+ return null;
161
+ const result = {};
162
+ for (const row of rows) {
163
+ const candidates = columns_to_types(row.DATA_TYPE, row.COLUMN_TYPE);
164
+ if (candidates.length > 0)
165
+ result[row.COLUMN_NAME] = candidates;
166
+ }
167
+ return result;
168
+ },
169
+ alter: async (table, diff) => {
170
+ const existing = await this.schema.introspect(table);
171
+ if (existing === null)
172
+ throw E.table_not_found(table);
173
+ for (const [from, to] of diff.rename) {
174
+ await this.#sql(Q `
175
+ ALTER TABLE ${table}
176
+ RENAME COLUMN ${[sql.quote(from)]} TO ${[sql.quote(to)]}`);
177
+ }
178
+ const parts = [];
179
+ for (const field of diff.drop) {
180
+ parts.push(`DROP COLUMN ${sql.quote(field)}`);
181
+ }
182
+ for (const [field, type] of Object.entries(diff.add)) {
183
+ parts.push(`ADD COLUMN ${sql.quote(field)} ${get_column(type)}`);
184
+ }
185
+ if (parts.length > 0) {
186
+ await this.#sql(Q `ALTER TABLE ${table} ${[parts.join(", ")]}`);
187
+ }
188
+ },
117
189
  };
118
190
  }
119
191
  async #where(as, where) {
@@ -141,7 +213,9 @@ export default class MySQL {
141
213
  const numeric = is_bigint_key(datatype);
142
214
  const lhs = numeric ? bigint_cast(quoted) : quoted;
143
215
  const rhs = numeric ? bigint_cast(ph) : ph;
144
- binds[bind_key] = await bind_value(datatype, op_value);
216
+ if (op !== "$in") {
217
+ binds[bind_key] = await bind_value(datatype, op_value);
218
+ }
145
219
  switch (op) {
146
220
  case "$like":
147
221
  parts.push(`${quoted} LIKE BINARY ${ph}`);
@@ -162,6 +236,19 @@ export default class MySQL {
162
236
  parts.push(`${lhs} ${sql_op} ${rhs}`);
163
237
  break;
164
238
  }
239
+ case "$in": {
240
+ const array = op_value;
241
+ const placeholders = [];
242
+ for (let i = 0; i < array.length; i++) {
243
+ const key = `${sql.bindKey(field, `in${i}`)}`;
244
+ binds[key] = await bind_value(datatype, array[i]);
245
+ const ph = `${BIND_BY}${key}`;
246
+ placeholders.push(is_bigint_key(datatype) ? bigint_cast(ph) : ph);
247
+ }
248
+ const lhs = is_bigint_key(datatype) ? bigint_cast(quoted) : quoted;
249
+ parts.push(`${lhs} IN (${placeholders.join(", ")})`);
250
+ break;
251
+ }
165
252
  default:
166
253
  throw E.operator_unknown(field, op);
167
254
  }
@@ -193,9 +280,9 @@ export default class MySQL {
193
280
  const pk = as.pk;
194
281
  const type = as.types[pk];
195
282
  const table = as.table;
196
- if (type === "string")
197
- return crypto.randomUUID();
198
- if (common.BIGINT_STRING_TYPES.includes(type)) {
283
+ if (base.is_uuid_type(type))
284
+ return base.generate_uuid(type);
285
+ if (base.BIGINT_STRING_TYPES.includes(type)) {
199
286
  const cast = bigint_cast(sql.quote(pk));
200
287
  const q = `SELECT MAX(${cast}) AS v FROM ${sql.quote(table)}`;
201
288
  const rows = await this.#sql(q);
@@ -235,7 +322,7 @@ export default class MySQL {
235
322
  throw E.pk_required(pk);
236
323
  const type = as.types[pk];
237
324
  // integer types, use AUTO_INCREMENT
238
- if (!common.BIGINT_STRING_TYPES.includes(type) && type !== "string") {
325
+ if (!base.BIGINT_STRING_TYPES.includes(type) && !base.is_uuid_type(type)) {
239
326
  const [keys, values] = this.#create(record);
240
327
  const query = keys.length > 0
241
328
  ? Q `INSERT INTO ${table} (${keys}) VALUES (${values})`
@@ -257,7 +344,7 @@ export default class MySQL {
257
344
  this.#explain = {};
258
345
  if (args.count === true)
259
346
  return this.#count(as, args.where);
260
- if (common.withed(args)) {
347
+ if (base.withed(args)) {
261
348
  return sql.joinable(as, args.with)
262
349
  ? this.#read_joined(as, args)
263
350
  : this.#read_phased(as, args);
@@ -273,15 +360,16 @@ export default class MySQL {
273
360
  return Number(rows[0]?.n ?? 0);
274
361
  }
275
362
  async #base_query(as, args) {
276
- const SELECT = args.fields === undefined
363
+ const SELECT = is.undefined(args.fields)
277
364
  ? "*"
278
365
  : args.fields.map(sql.quote).join(", ");
279
366
  const [WHERE, binds] = await this.#where(as, args.where);
280
367
  const ORDER_BY = order_by(as.types, args.sort);
281
- const LIMIT = sql.limit(args.limit);
368
+ const LIMIT = is.defined(args.limit) ? ` LIMIT ${args.limit}` : "";
369
+ const OFFSET = is.defined(args.offset) ? ` OFFSET ${args.offset}` : "";
282
370
  const table = sql.quote(as.table);
283
371
  return [
284
- `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}`,
372
+ `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}${OFFSET}`,
285
373
  binds,
286
374
  ];
287
375
  }
@@ -292,9 +380,9 @@ export default class MySQL {
292
380
  return rows.map(r => unbind(as.types, r));
293
381
  }
294
382
  async #read_phased(as, args) {
295
- const fields = common.expand(as, args.fields, args.with);
383
+ const fields = base.expand(as, args.fields, args.with);
296
384
  const rows = await this.#read(as, { ...args, fields });
297
- const out = rows.map(row => common.project(row, args.fields));
385
+ const out = rows.map(row => base.project(row, args.fields));
298
386
  for (const [table, relation] of Object.entries(args.with)) {
299
387
  await this.#attach_relation(as, { rows, out, table, relation });
300
388
  }
@@ -330,8 +418,8 @@ export default class MySQL {
330
418
  }
331
419
  const rows = grouped.get(join_value) ?? [];
332
420
  args.out[i][args.table] = is_many
333
- ? rows.map(r => common.project(r, relation.fields))
334
- : rows[0] ? common.project(rows[0], relation.fields) : null;
421
+ ? rows.map(r => base.project(r, relation.fields))
422
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
335
423
  }
336
424
  }
337
425
  async #load_related(args) {
@@ -357,7 +445,7 @@ export default class MySQL {
357
445
  : [in_part];
358
446
  const WHERE = `WHERE ${where_parts.join(" AND ")}`;
359
447
  const all_columns = Object.keys(args.as.types);
360
- const fields = args.fields !== undefined && args.fields.length > 0
448
+ const fields = is.defined(args.fields) && is.uint(args.fields.length)
361
449
  ? args.fields
362
450
  : all_columns;
363
451
  const select_fields = [...new Set([...fields, args.by])];
@@ -370,7 +458,7 @@ export default class MySQL {
370
458
  : ` ORDER BY ${base_order}`;
371
459
  const table = sql.quote(args.as.table);
372
460
  let query;
373
- if (per_parent !== undefined) {
461
+ if (is.defined(per_parent)) {
374
462
  const partition = numeric ? cast : Q `${args.by}`;
375
463
  const rn_order = user_order ? ` ORDER BY ${user_order}` : "";
376
464
  query = `
@@ -416,13 +504,13 @@ export default class MySQL {
416
504
  const tables = [as.table, ...Object.values(args.with).map(r => r.as.table)];
417
505
  const aliases = sql.aliases(tables);
418
506
  const alias = aliases[as.table];
419
- const fields = common.fields(args.fields, as.pk) ?? Object.keys(as.types);
507
+ const fields = base.fields(args.fields, as.pk) ?? Object.keys(as.types);
420
508
  const SELECT = [
421
509
  ...fields.map(f => `${alias}.${sql.quote(f)} AS ${alias}_${f}`),
422
510
  ...Object.values(args.with).flatMap(relation => {
423
511
  const r_alias = aliases[relation.as.table];
424
512
  const r_fields = relation.as.pk !== null
425
- ? common.fields(relation.fields, relation.as.pk)
513
+ ? base.fields(relation.fields, relation.as.pk)
426
514
  : relation.fields;
427
515
  return (r_fields ?? Object.keys(relation.as.types))
428
516
  .map(f => `${r_alias}.${sql.quote(f)} AS ${r_alias}_${f}`);
@@ -457,5 +545,32 @@ export default class MySQL {
457
545
  const result = await this.#execute(query, binds);
458
546
  return result.affectedRows;
459
547
  }
548
+ sql(options) {
549
+ if (is.defined(options.input)) {
550
+ const placeholders = new Set([...options.query.matchAll(/:(\w+)/g)].map(m => m[1]));
551
+ const input_keys = new Set(Object.keys(options.input.properties));
552
+ for (const key of input_keys) {
553
+ if (!placeholders.has(key))
554
+ throw E.sql_placeholder_missing(key);
555
+ }
556
+ for (const key of placeholders) {
557
+ if (!input_keys.has(key))
558
+ throw E.sql_input_missing(key);
559
+ }
560
+ }
561
+ return async (args) => {
562
+ const input = is.defined(options.input)
563
+ ? options.input.parse(args)
564
+ : undefined;
565
+ const binds = is.defined(input)
566
+ ? await this.#create_params(Object.fromEntries(Object.entries(options.input.properties)
567
+ .map(([k, v]) => [k, v.datatype])), input)
568
+ : {};
569
+ const rows = await this.#sql(options.query, binds);
570
+ return (is.defined(options.output)
571
+ ? options.output.parse(rows)
572
+ : undefined);
573
+ };
574
+ }
460
575
  }
461
576
  //# sourceMappingURL=MySQL.js.map
@@ -12,6 +12,18 @@ function number(column) {
12
12
  unbind: (value) => Number(value),
13
13
  };
14
14
  }
15
+ function uuid_to_bin(uuid) {
16
+ return Uint8Array.fromHex(uuid.replace(/-/g, ""));
17
+ }
18
+ function bin_to_uuid(buf) {
19
+ const hex = buf.toHex();
20
+ const a = hex.slice(0, 8);
21
+ const b = hex.slice(8, 12);
22
+ const c = hex.slice(12, 16);
23
+ const d = hex.slice(16, 20);
24
+ const e = hex.slice(20);
25
+ return `${a}-${b}-${c}-${d}-${e}`;
26
+ }
15
27
  const typemap = {
16
28
  blob: {
17
29
  async bind(value) {
@@ -55,16 +67,15 @@ const typemap = {
55
67
  },
56
68
  },
57
69
  i8: number("TINYINT"),
58
- /*primary: {
59
- bind(value) {
60
- if (is.numeric(value)) return Number(value);
61
- throw new Error(`\`${value}\` is not a valid primary key value`);
62
- },
63
- column: "INT NOT NULL AUTO_INCREMENT PRIMARY KEY",
64
- unbind(value) {
65
- return String(value);
66
- },
67
- },*/
70
+ json: {
71
+ bind(value) {
72
+ return JSON.stringify(value);
73
+ },
74
+ column: "JSON",
75
+ unbind(value) {
76
+ return JSON.parse(value);
77
+ },
78
+ },
68
79
  string: identity("TEXT"),
69
80
  time: identity("TEXT"),
70
81
  u128: {
@@ -97,6 +108,21 @@ const typemap = {
97
108
  return new URL(value);
98
109
  },
99
110
  },
111
+ uuid: {
112
+ bind: uuid_to_bin,
113
+ column: "BINARY(16)",
114
+ unbind: bin_to_uuid,
115
+ },
116
+ uuid_v4: {
117
+ bind: uuid_to_bin,
118
+ column: "BINARY(16)",
119
+ unbind: bin_to_uuid,
120
+ },
121
+ uuid_v7: {
122
+ bind: uuid_to_bin,
123
+ column: "BINARY(16)",
124
+ unbind: bin_to_uuid,
125
+ },
100
126
  };
101
127
  export default typemap;
102
128
  //# sourceMappingURL=typemap.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primate/mysql",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "MySQL databases for Primate",
5
5
  "homepage": "https://primate.run/docs/database/mysql",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",
@@ -17,27 +17,27 @@
17
17
  "!/**/*.spec.*"
18
18
  ],
19
19
  "devDependencies": {
20
- "@rcompat/type": "^0.9.0"
20
+ "@rcompat/type": "^0.17.0"
21
21
  },
22
22
  "dependencies": {
23
- "@rcompat/assert": "^0.6.0",
24
- "@rcompat/is": "^0.4.2",
25
- "mysql2": "^3.17.1",
26
- "@primate/core": "^0.5.0",
27
- "pema": "0.5.0"
23
+ "@rcompat/assert": "^0.13.0",
24
+ "@rcompat/is": "^0.11.0",
25
+ "mysql2": "^3.22.3",
26
+ "@primate/core": "^0.7.0",
27
+ "pema": "0.7.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "primate": "^0.36.0"
30
+ "primate": "^0.38.0"
31
31
  },
32
32
  "imports": {
33
33
  "#*": {
34
- "apekit": "./src/private/*.ts",
34
+ "@primate/source": "./src/private/*.ts",
35
35
  "default": "./lib/private/*.js"
36
36
  }
37
37
  },
38
38
  "exports": {
39
39
  ".": {
40
- "apekit": "./src/public/index.ts",
40
+ "@primate/source": "./src/public/index.ts",
41
41
  "default": "./lib/public/index.js"
42
42
  }
43
43
  },
@@ -45,6 +45,10 @@
45
45
  "build": "npm run clean && tsc",
46
46
  "clean": "rm -rf ./lib",
47
47
  "lint": "eslint .",
48
- "test": "npm run build && npx proby"
48
+ "mysql": "scripts/mysql.sh",
49
+ "mariadb": "scripts/mariadb.sh",
50
+ "test:mysql": "npm run mysql && npx proby",
51
+ "test:mariadb": "npm run mariadb && npx proby",
52
+ "test": "npm run test:mysql"
49
53
  }
50
54
  }