@revojs/storage 0.2.8 → 0.2.9

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.
@@ -2,6 +2,7 @@ declare const _default: import("revojs/module").Command<{
2
2
  readonly directory: {
3
3
  readonly type: "string";
4
4
  readonly short: "d";
5
+ readonly default: "migrations";
5
6
  readonly description: "Migration output directory";
6
7
  };
7
8
  }>;
@@ -1,45 +1,63 @@
1
- import { VARIABLE_CONTEXT } from "revojs";
2
- import storage from "#storage";
1
+ import { STORAGE_CONTEXT } from "../index.js";
2
+ import { getContext } from "revojs";
3
3
  import { defineCommand } from "revojs/module";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
5
5
  import { join, resolve } from "path";
6
- import { loadEnv } from "vite";
7
6
  //#region src/commands/generate.ts
8
7
  var generate_default = defineCommand({
9
8
  description: "Generate migration files from the storage schema",
10
9
  arguments: { directory: {
11
10
  type: "string",
12
11
  short: "d",
12
+ default: "migrations",
13
13
  description: "Migration output directory"
14
14
  } },
15
- async run({ arguments: { directory: output = "migrations" }, environment }) {
16
- const databases = Object.values(storage).flatMap((source) => Object.entries(source));
15
+ async run(scope, context) {
16
+ const databases = new Array();
17
+ const names = /* @__PURE__ */ new Set();
18
+ for (const files of Object.values(getContext(scope, STORAGE_CONTEXT))) for (const [name, database] of Object.entries(files)) {
19
+ if (names.has(name)) throw new Error("Migration database names must be unique across storage scopes.");
20
+ names.add(name);
21
+ databases.push({
22
+ name,
23
+ database
24
+ });
25
+ }
17
26
  if (databases.length === 0) {
18
27
  console.log("No storage databases found.");
19
28
  return;
20
29
  }
21
- const scope = {
22
- hooks: {},
23
- context: { [VARIABLE_CONTEXT]: loadEnv(environment.config.mode, environment.config.envDir, "") }
24
- };
25
- for (const [name, load] of databases) {
26
- const database = (await load())(scope);
30
+ for (const { name, database } of databases) {
27
31
  const current = database.toMeta();
28
- const directory = resolve(output, name);
29
- const snapshot = (existsSync(directory) ? readdirSync(directory) : []).sort().reverse().map((entry) => join(directory, entry, "meta.json")).find((path) => existsSync(path));
32
+ const directory = resolve(context.arguments.directory, name);
33
+ let snapshot;
34
+ if (existsSync(directory)) {
35
+ const entries = readdirSync(directory).sort();
36
+ for (let index = entries.length - 1; index >= 0; index--) {
37
+ const path = join(directory, entries[index], "meta.json");
38
+ if (existsSync(path)) {
39
+ snapshot = path;
40
+ break;
41
+ }
42
+ }
43
+ }
30
44
  const changes = database.$migration.diff(snapshot ? JSON.parse(readFileSync(snapshot, "utf8")) : void 0, current);
31
45
  if (changes.length === 0) {
32
46
  console.log(`No schema changes detected for "${name}".`);
33
47
  continue;
34
48
  }
49
+ const statements = database.$migration.toSql(changes, current);
50
+ if (statements.length === 0) {
51
+ console.log(`No SQL schema changes detected for "${name}".`);
52
+ continue;
53
+ }
35
54
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
36
55
  let order = timestamp;
37
56
  for (let index = 2; existsSync(join(directory, order)); index++) order = `${timestamp}-${index}`;
38
57
  const target = join(directory, order);
39
- const statements = database.$migration.toSql(changes, current);
40
58
  mkdirSync(target, { recursive: true });
41
59
  writeFileSync(join(target, "meta.json"), `${JSON.stringify(current, null, 4)}\n`);
42
- writeFileSync(join(target, "migration.sql"), statements.length ? `${statements.join("\n\n")}\n` : "");
60
+ writeFileSync(join(target, "migration.sql"), `${statements.join("\n\n")}\n`);
43
61
  console.log(`Created migration "${name}/${order}" with ${changes.length} change(s).`);
44
62
  }
45
63
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Scope, type Source, type SourceScope, type SourceValue } from "revojs";
2
- import type { Primitive } from "revojs/shared";
1
+ import { type Resolvable, type Scope, type Source, type SourceScope, type SourceValue } from "revojs";
2
+ import type { Awaitable, Primitive } from "revojs/shared";
3
3
  export type DefaultValue<T> = undefined | T | (() => T);
4
4
  export type References = undefined | (() => Column);
5
5
  export type ReferentialAction = "cascade" | "restrict" | "no action" | "set null" | "set default";
@@ -165,6 +165,7 @@ export declare abstract class Statement<T = unknown> extends Sql<T> {
165
165
  protected $session: Session;
166
166
  protected $fields: Array<[string, Column]>;
167
167
  constructor(session: Session, value: Table | Record<string, Column>, ...chunks: Array<Chunk>);
168
+ run(): unknown;
168
169
  then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
169
170
  protected mapRow(row: Array<unknown>): Record<string, unknown>;
170
171
  }
@@ -176,6 +177,8 @@ export declare class SelectStatement<TSelection = Table | Record<string, Column>
176
177
  table: Table;
177
178
  nullable: boolean;
178
179
  }[];
180
+ protected $limit: Sql | undefined;
181
+ protected $offset: Sql | undefined;
179
182
  constructor(session: Session, value: TSelection);
180
183
  from(table: Table): this & Record<"from", never>;
181
184
  leftJoin<TTable extends Table>(table: TTable, on: Chunk): SelectStatement<AppendToSelection<TSelection, TTable, TMode>, TMode extends "partial" ? "partial" : "multiple", AppendToNullabilityMap<TNullability, TTable, "left">>;
@@ -187,6 +190,7 @@ export declare class SelectStatement<TSelection = Table | Record<string, Column>
187
190
  having(chunk: Chunk): this & Record<"having", never>;
188
191
  orderBy(...chunks: Array<Chunk>): this & Record<"orderBy", never>;
189
192
  limit(count: number): this & Record<"limit", never>;
193
+ first(): Promise<SelectResult<TSelection, TMode, TNullability> | undefined>;
190
194
  offset(count: number): this & Record<"offset", never>;
191
195
  protected mapRow(row: Array<unknown>): Record<string, unknown>;
192
196
  }
@@ -220,6 +224,7 @@ export declare abstract class PreparedQuery {
220
224
  abstract values(parameters: Array<Primitive>): unknown;
221
225
  }
222
226
  export declare abstract class Session {
227
+ transaction<T>(run: (session: Session) => Awaitable<T>): Promise<T>;
223
228
  run(query: Query): unknown;
224
229
  all(query: Query): unknown;
225
230
  get(query: Query): unknown;
@@ -270,14 +275,17 @@ export declare abstract class Database<T extends Record<string, Table> = Record<
270
275
  abstract insert<TValue extends Table>(table: TValue): InsertStatement<TValue>;
271
276
  abstract update<TValue extends Table>(table: TValue): UpdateStatement<TValue>;
272
277
  abstract delete<TValue extends Table>(table: TValue): DeleteStatement<TValue>;
278
+ abstract transaction<TResult>(run: (database: Database<T>) => Awaitable<TResult>): Promise<TResult>;
273
279
  }
274
280
  export declare abstract class Table<TName extends string = string, TColumns extends Record<string, Column> = Record<string, Column>> {
275
281
  readonly $name: TName;
276
282
  readonly $columns: TColumns;
277
283
  readonly $definitions: Array<Definition>;
284
+ readonly $base: Table | undefined;
278
285
  constructor(name: TName, columns: TColumns, definitions: Array<Definition>);
279
286
  toMeta(): TableMeta;
280
287
  toSql(): Sql<unknown>;
288
+ hasReference(reference: Table): boolean;
281
289
  }
282
290
  export declare abstract class Column<TDataType = unknown> {
283
291
  $name: string;
@@ -322,7 +330,11 @@ export declare class PrimaryKeyConstraint {
322
330
  export declare class ForeignKeyConstraint {
323
331
  $columns: Array<Column>;
324
332
  $foreignColumns: Array<Column>;
333
+ $onDelete: ReferentialAction | undefined;
334
+ $onUpdate: ReferentialAction | undefined;
325
335
  constructor(columns: Array<Column>, foreignColumns: Array<Column>);
336
+ onDelete(action: ReferentialAction): this;
337
+ onUpdate(action: ReferentialAction): this;
326
338
  toMeta(): ForeignKeyConstraintMeta;
327
339
  }
328
340
  export declare class Index {
@@ -355,6 +367,11 @@ export declare function and<T>(...chunks: Array<Chunk>): Sql<T>;
355
367
  export declare function or<T>(...chunks: Array<Chunk>): Sql<T>;
356
368
  export declare function asc<T>(chunk: Chunk): Sql<T>;
357
369
  export declare function desc<T>(chunk: Chunk): Sql<T>;
358
- export declare function useStorage<TSource extends Source<(scope: Scope) => Database> = typeof import("#storage").default, TScope extends SourceScope<TSource> = SourceScope<TSource>>(scope: Scope, scopeName: TScope): { [TKey in SourceValue<TSource, TScope>]: ReturnType<Awaited<ReturnType<TSource[TScope][TKey]>>>; };
370
+ export declare function mergeTable<TTable extends Table, TColumns extends Record<string, Column>>(table: TTable, columns: TColumns, definitions?: (columns: TTable["$columns"] & TColumns) => Array<Definition>): TTable & TColumns & {
371
+ readonly $columns: TTable["$columns"] & TColumns;
372
+ };
373
+ export declare function useReferenceStorage<TSchema extends Record<string, Table>>(scope: Scope, references: TSchema): Database<TSchema>;
374
+ export declare function useStorageProvider<TProvider>(scope: Scope, providers: ReadonlyArray<TProvider>, getReferences: (provider: TProvider) => Record<string, Table>): TProvider;
375
+ export declare function useStorage<TSource extends Source<Resolvable<Awaitable<Database>>> = typeof import("#storage").default, TScope extends SourceScope<TSource> = SourceScope<TSource>>(scope: Scope, scopeName: TScope): { [TKey in SourceValue<TSource, TScope>]: Awaited<ReturnType<Awaited<ReturnType<TSource[TScope][TKey]>>>>; };
359
376
  export declare const STORAGE_CONTEXT: import("revojs").Descriptor<Record<string, Record<string, Database<Record<string, Table<string, Record<string, Column<unknown>>>>>>>>;
360
377
  export {};
package/dist/index.js CHANGED
@@ -51,6 +51,9 @@ var Statement = class extends Sql {
51
51
  this.$session = session;
52
52
  this.$fields = Object.entries(value instanceof Table ? value.$columns : value);
53
53
  }
54
+ run() {
55
+ return this.$session.run(this.toQuery());
56
+ }
54
57
  async then(onfulfilled, onrejected) {
55
58
  let value;
56
59
  try {
@@ -75,6 +78,8 @@ var SelectStatement = class extends Statement {
75
78
  $selection;
76
79
  $baseNullable = false;
77
80
  $joins = new Array();
81
+ $limit;
82
+ $offset;
78
83
  constructor(session, value) {
79
84
  super(session, value);
80
85
  this.$value = value;
@@ -148,11 +153,18 @@ var SelectStatement = class extends Statement {
148
153
  return this;
149
154
  }
150
155
  limit(count) {
151
- this.push(sql`LIMIT ${new Parameter(count)}`);
156
+ this.$limit = sql`LIMIT ${new Parameter(count)}`;
157
+ this.push(this.$limit);
152
158
  return this;
153
159
  }
160
+ async first() {
161
+ const query = this.$limit ? this.toQuery() : new Sql(...this.$chunks.filter((chunk) => chunk !== this.$offset)).push(sql`LIMIT ${new Parameter(1)}`, ...this.$offset ? [this.$offset] : []).toQuery();
162
+ const row = await this.$session.get(query);
163
+ return row ? this.mapRow(row) : void 0;
164
+ }
154
165
  offset(count) {
155
- this.push(sql`OFFSET ${new Parameter(count)}`);
166
+ this.$offset = sql`OFFSET ${new Parameter(count)}`;
167
+ this.push(this.$offset);
156
168
  return this;
157
169
  }
158
170
  mapRow(row) {
@@ -248,6 +260,9 @@ var PreparedQuery = class {
248
260
  }
249
261
  };
250
262
  var Session = class {
263
+ async transaction(run) {
264
+ throw new Error("This storage provider does not support interactive transactions.");
265
+ }
251
266
  run(query) {
252
267
  return this.prepareQuery(query).run(query.parameters);
253
268
  }
@@ -387,18 +402,16 @@ var Migration = class {
387
402
  }
388
403
  toCreateTableChange(change, name = change.table.name) {
389
404
  const definitions = new Array();
390
- for (const column of Object.values(change.table.columns)) {
391
- definitions.push(...this.toCreateColumnChange({
392
- kind: "createColumn",
393
- table: change.table.name,
394
- column
395
- }, true));
396
- for (const constraint of this.columnConstraints(column)) definitions.push(...this.toCreateConstraintChange({
397
- kind: "createConstraint",
398
- table: change.table.name,
399
- constraint
400
- }, true));
401
- }
405
+ for (const column of Object.values(change.table.columns)) definitions.push(...this.toCreateColumnChange({
406
+ kind: "createColumn",
407
+ table: change.table.name,
408
+ column
409
+ }, true));
410
+ for (const column of Object.values(change.table.columns)) for (const constraint of this.columnConstraints(column)) definitions.push(...this.toCreateConstraintChange({
411
+ kind: "createConstraint",
412
+ table: change.table.name,
413
+ constraint
414
+ }, true));
402
415
  for (const definition of change.table.definitions) if (definition.kind !== "index" && definition.kind !== "uniqueIndex") definitions.push(...this.toCreateConstraintChange({
403
416
  kind: "createConstraint",
404
417
  table: change.table.name,
@@ -584,6 +597,7 @@ var Table = class {
584
597
  $name;
585
598
  $columns;
586
599
  $definitions;
600
+ $base;
587
601
  constructor(name, columns, definitions) {
588
602
  this.$name = name;
589
603
  this.$columns = columns;
@@ -600,6 +614,15 @@ var Table = class {
600
614
  toSql() {
601
615
  return new Sql(`"${this.$name}"`);
602
616
  }
617
+ hasReference(reference) {
618
+ if (this === reference) return true;
619
+ let table = this.$base;
620
+ while (table) {
621
+ if (table === reference) return true;
622
+ table = table.$base;
623
+ }
624
+ return false;
625
+ }
603
626
  };
604
627
  var Column = class {
605
628
  $name;
@@ -719,10 +742,20 @@ var PrimaryKeyConstraint = class {
719
742
  var ForeignKeyConstraint = class {
720
743
  $columns;
721
744
  $foreignColumns;
745
+ $onDelete;
746
+ $onUpdate;
722
747
  constructor(columns, foreignColumns) {
723
748
  this.$columns = columns;
724
749
  this.$foreignColumns = foreignColumns;
725
750
  }
751
+ onDelete(action) {
752
+ this.$onDelete = action;
753
+ return this;
754
+ }
755
+ onUpdate(action) {
756
+ this.$onUpdate = action;
757
+ return this;
758
+ }
726
759
  toMeta() {
727
760
  const foreignTables = new Set(this.$foreignColumns.map((column) => column.$table?.$name));
728
761
  if (foreignTables.size !== 1 || foreignTables.has(void 0)) throw new Error("Foreign key columns must belong to the same table");
@@ -730,7 +763,9 @@ var ForeignKeyConstraint = class {
730
763
  kind: "foreignKey",
731
764
  columns: this.$columns.map((column) => column.$name),
732
765
  foreignTable: [...foreignTables][0],
733
- foreignColumns: this.$foreignColumns.map((column) => column.$name)
766
+ foreignColumns: this.$foreignColumns.map((column) => column.$name),
767
+ onDelete: this.$onDelete,
768
+ onUpdate: this.$onUpdate
734
769
  };
735
770
  }
736
771
  };
@@ -830,6 +865,64 @@ function asc(chunk) {
830
865
  function desc(chunk) {
831
866
  return sql`${chunk} DESC`;
832
867
  }
868
+ function mergeTable(table, columns, definitions) {
869
+ const mergedColumns = {};
870
+ const names = /* @__PURE__ */ new Set();
871
+ for (const [key, column] of Object.entries(table.$columns)) {
872
+ mergedColumns[key] = Object.assign(Object.create(Object.getPrototypeOf(column)), column);
873
+ names.add(column.$name);
874
+ }
875
+ for (const [key, column] of Object.entries(columns)) {
876
+ if (key in table) throw new Error(`Cannot replace table field "${key}" when extending "${table.$name}".`);
877
+ if (names.has(column.$name)) throw new Error(`Duplicate SQL column "${column.$name}" when extending "${table.$name}".`);
878
+ mergedColumns[key] = Object.assign(Object.create(Object.getPrototypeOf(column)), column);
879
+ names.add(column.$name);
880
+ }
881
+ const merged = Object.create(Object.getPrototypeOf(table));
882
+ Object.assign(merged, table, mergedColumns, {
883
+ $base: table,
884
+ $columns: mergedColumns,
885
+ $definitions: [...table.$definitions]
886
+ });
887
+ for (const column of Object.values(mergedColumns)) column.$table = merged;
888
+ if (definitions) merged.$definitions.push(...definitions(merged.$columns));
889
+ return merged;
890
+ }
891
+ function useReferenceStorage(scope, references) {
892
+ const tables = Object.values(references);
893
+ if (tables.length === 0) throw new Error("Expected at least one table reference.");
894
+ const databases = new Array();
895
+ for (const files of Object.values(getContext(scope, STORAGE_CONTEXT))) for (const database of Object.values(files)) {
896
+ let matches = true;
897
+ for (const reference of tables) {
898
+ let found = false;
899
+ for (const table of Object.values(database.$schema)) if (table.hasReference(reference)) {
900
+ found = true;
901
+ break;
902
+ }
903
+ if (!found) {
904
+ matches = false;
905
+ break;
906
+ }
907
+ }
908
+ if (matches) databases.push(database);
909
+ }
910
+ if (databases.length !== 1) throw new Error(`Expected one initialized storage containing tables ${tables.map((table) => `"${table.$name}"`).join(", ")}, found ${databases.length}.`);
911
+ for (const [key, table] of Object.entries(references)) if (!databases[0].$schema[key]?.hasReference(table)) throw new Error(`Storage schema key "${key}" does not contain the supplied table reference.`);
912
+ return databases[0];
913
+ }
914
+ function useStorageProvider(scope, providers, getReferences) {
915
+ const databases = Object.values(getContext(scope, STORAGE_CONTEXT)).flatMap((files) => Object.values(files));
916
+ const matches = new Array();
917
+ for (const provider of providers) {
918
+ const tables = Object.values(getReferences(provider));
919
+ if (tables.length === 0) throw new Error("Expected at least one table reference.");
920
+ for (const database of databases) if (tables.every((reference) => Object.values(database.$schema).some((table) => table.hasReference(reference)))) matches.push(provider);
921
+ }
922
+ if (matches.length !== 1) throw new Error(`Expected one initialized storage matching a provider, found ${matches.length}.`);
923
+ useReferenceStorage(scope, getReferences(matches[0]));
924
+ return matches[0];
925
+ }
833
926
  function useStorage(scope, scopeName) {
834
927
  const storages = getContext(scope, STORAGE_CONTEXT);
835
928
  const storage = storages[scopeName] ?? {};
@@ -839,4 +932,4 @@ function useStorage(scope, scopeName) {
839
932
  }
840
933
  var STORAGE_CONTEXT = defineContext("STORAGE_CONTEXT");
841
934
  //#endregion
842
- export { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, Parameter, PreparedQuery, PrimaryKeyConstraint, STORAGE_CONTEXT, SelectStatement, Session, Sql, Statement, Table, UniqueConstraint, UniqueIndex, UpdateStatement, and, asc, between, combine, contains, desc, equals, exists, greater, greaterEquals, like, lower, lowerEquals, not, nullish, or, sql, toParameter, useStorage };
935
+ export { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, Parameter, PreparedQuery, PrimaryKeyConstraint, STORAGE_CONTEXT, SelectStatement, Session, Sql, Statement, Table, UniqueConstraint, UniqueIndex, UpdateStatement, and, asc, between, combine, contains, desc, equals, exists, greater, greaterEquals, like, lower, lowerEquals, mergeTable, not, nullish, or, sql, toParameter, useReferenceStorage, useStorage, useStorageProvider };
@@ -1,3 +1,14 @@
1
- export type StorageConfig = Record<string, never>;
2
- declare const _default: import("revojs/module").Module<"storage", StorageConfig, []>;
1
+ declare const _default: import("revojs/module").Module<"storage", {
2
+ storage: {
3
+ match: ("!**/*.d.ts" | "**/*.js" | "**/*.ts")[];
4
+ from: "./storage"[];
5
+ format: "object";
6
+ };
7
+ plugins: {
8
+ from: string[];
9
+ };
10
+ commands: {
11
+ from: string[];
12
+ };
13
+ }, []>;
3
14
  export default _default;
@@ -2,12 +2,15 @@ import { defineModule, useModule } from "revojs/module";
2
2
  //#region src/module/index.ts
3
3
  var module_default = defineModule({
4
4
  name: "storage",
5
- config: () => ({}),
6
5
  setup() {
7
6
  const { fromModule } = useModule(import.meta.url);
8
7
  return { sources: {
9
8
  storage: {
10
- match: ["**/*.js", "**/*.ts"],
9
+ match: [
10
+ "**/*.js",
11
+ "**/*.ts",
12
+ "!**/*.d.ts"
13
+ ],
11
14
  from: ["./storage"],
12
15
  format: "object"
13
16
  },
@@ -1,15 +1,20 @@
1
1
  import { STORAGE_CONTEXT } from "../index.js";
2
2
  import { definePlugin, getContext, setContext } from "revojs";
3
- import storage from "#storage";
3
+ import { isServer } from "#variables";
4
4
  //#region src/plugins/index.ts
5
5
  var plugins_default = definePlugin({ async setup(scope) {
6
- const storages = getContext(scope, STORAGE_CONTEXT);
7
- for (const [scopeName, files] of Object.entries(storage)) {
8
- const databases = storages[scopeName] ?? {};
9
- for (const [key, resolve] of Object.entries(files)) databases[key] = (await resolve())(scope);
10
- storages[scopeName] = databases;
6
+ if (isServer) {
7
+ const databases = getContext(scope, STORAGE_CONTEXT);
8
+ for (const [scopeName, files] of Object.entries((await import("#storage")).default)) {
9
+ const entries = databases[scopeName] ?? {};
10
+ for (const [name, resolve] of Object.entries(files)) {
11
+ if (entries[name]) continue;
12
+ entries[name] = await (await resolve())(scope);
13
+ }
14
+ databases[scopeName] = entries;
15
+ }
16
+ setContext(scope, STORAGE_CONTEXT, databases);
11
17
  }
12
- setContext(scope, STORAGE_CONTEXT, storages);
13
18
  } });
14
19
  //#endregion
15
20
  export { plugins_default as default };
@@ -1,4 +1,5 @@
1
- import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Table, UniqueConstraint, UniqueIndex, UpdateStatement, type Definition, type DeleteConstraintChange, type DeleteIndexChange, type Session, type Sql, type UpdateColumnChange } from "../index";
1
+ import type { Awaitable } from "revojs/shared";
2
+ import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Sql, Table, UniqueConstraint, UniqueIndex, UpdateStatement, type Definition, type DeleteConstraintChange, type DeleteIndexChange, type Session, type UpdateColumnChange } from "../index";
2
3
  export interface CharOptions {
3
4
  length?: number;
4
5
  }
@@ -12,6 +13,7 @@ export interface VarcharOptions {
12
13
  export declare abstract class MysqlColumn<TDataType = unknown> extends Column<TDataType> {
13
14
  autoincrement<This extends MysqlColumn>(this: This): This & Record<"autoincrement", never>;
14
15
  type<TData>(): this & Record<"$type", TData>;
16
+ toSqlName(): Sql<unknown>;
15
17
  }
16
18
  export declare class BigintColumn extends MysqlColumn<bigint> {
17
19
  toSqlType(): string;
@@ -31,6 +33,8 @@ export declare class DateColumn extends MysqlColumn<Date> {
31
33
  toSqlType(): string;
32
34
  }
33
35
  export declare class DatetimeColumn extends MysqlColumn<Date> {
36
+ fromDriverValue(value: unknown): Date;
37
+ toDriverValue(value: Date): string;
34
38
  toSqlType(): string;
35
39
  }
36
40
  export declare class DecimalColumn extends MysqlColumn<string> {
@@ -124,6 +128,7 @@ export declare class MysqlUpdateStatement<T extends Table> extends UpdateStateme
124
128
  export declare class MysqlDeleteStatement<T extends Table> extends DeleteStatement<T> {
125
129
  }
126
130
  export declare class MysqlTable<TName extends string = string, TColumns extends Record<string, MysqlColumn> = Record<string, MysqlColumn>> extends Table<TName, TColumns> {
131
+ toSql(): Sql<unknown>;
127
132
  }
128
133
  export declare class MysqlMigration extends Migration {
129
134
  readonly $dialect = "mysql";
@@ -139,6 +144,7 @@ export declare class MysqlDatabase<T extends Record<string, MysqlTable>> extends
139
144
  insert<T extends Table>(table: T): MysqlInsertStatement<T>;
140
145
  update<T extends Table>(table: T): MysqlUpdateStatement<T>;
141
146
  delete<T extends Table>(table: T): MysqlDeleteStatement<T>;
147
+ transaction<TResult>(run: (database: MysqlDatabase<T>) => Awaitable<TResult>): Promise<TResult>;
142
148
  }
143
149
  export declare function bigint(name: string): BigintColumn;
144
150
  export declare function binary(name: string): BinaryColumn;
@@ -1,4 +1,4 @@
1
- import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Table, UniqueConstraint, UniqueIndex, UpdateStatement } from "../index.js";
1
+ import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Sql, Table, UniqueConstraint, UniqueIndex, UpdateStatement } from "../index.js";
2
2
  //#region src/protocols/mysql.ts
3
3
  var MysqlColumn = class extends Column {
4
4
  autoincrement() {
@@ -7,6 +7,9 @@ var MysqlColumn = class extends Column {
7
7
  type() {
8
8
  return this;
9
9
  }
10
+ toSqlName() {
11
+ return new Sql(`\`${this.$name.replaceAll("`", "``")}\``);
12
+ }
10
13
  };
11
14
  var BigintColumn = class extends MysqlColumn {
12
15
  toSqlType() {
@@ -39,6 +42,12 @@ var DateColumn = class extends MysqlColumn {
39
42
  }
40
43
  };
41
44
  var DatetimeColumn = class extends MysqlColumn {
45
+ fromDriverValue(value) {
46
+ return value instanceof Date ? value : new Date(String(value).replace(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/, "$1T$2Z"));
47
+ }
48
+ toDriverValue(value) {
49
+ return value.toISOString().slice(0, 19).replace("T", " ");
50
+ }
42
51
  toSqlType() {
43
52
  return "datetime";
44
53
  }
@@ -183,7 +192,11 @@ var MysqlSelectStatement = class extends SelectStatement {};
183
192
  var MysqlInsertStatement = class extends InsertStatement {};
184
193
  var MysqlUpdateStatement = class extends UpdateStatement {};
185
194
  var MysqlDeleteStatement = class extends DeleteStatement {};
186
- var MysqlTable = class extends Table {};
195
+ var MysqlTable = class extends Table {
196
+ toSql() {
197
+ return new Sql(`\`${this.$name.replaceAll("`", "``")}\``);
198
+ }
199
+ };
187
200
  var MysqlMigration = class extends Migration {
188
201
  $dialect = "mysql";
189
202
  toUpdateColumnChange(change) {
@@ -205,7 +218,7 @@ var MysqlMigration = class extends Migration {
205
218
  return `\`${value.replaceAll("`", "``")}\``;
206
219
  }
207
220
  };
208
- var MysqlDatabase = class extends Database {
221
+ var MysqlDatabase = class MysqlDatabase extends Database {
209
222
  $migration;
210
223
  constructor(session, schema) {
211
224
  super(session, schema);
@@ -223,6 +236,9 @@ var MysqlDatabase = class extends Database {
223
236
  delete(table) {
224
237
  return new MysqlDeleteStatement(this.$session, table);
225
238
  }
239
+ transaction(run) {
240
+ return this.$session.transaction((session) => run(new MysqlDatabase(session, this.$schema)));
241
+ }
226
242
  };
227
243
  function bigint(name) {
228
244
  return new BigintColumn(name);
@@ -1,3 +1,4 @@
1
+ import type { Awaitable } from "revojs/shared";
1
2
  import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Table, UniqueConstraint, UniqueIndex, UpdateStatement, type AppendToNullabilityMap, type AppendToSelection, type Definition, type SelectMode, type Session, type Sql } from "../index";
2
3
  export type TimestampMode = "date" | "string";
3
4
  export interface CharOptions {
@@ -111,6 +112,8 @@ export declare class TimeColumn extends PostgresqlColumn<string> {
111
112
  export declare class TimestampColumn extends PostgresqlColumn<Date> {
112
113
  protected $options: TimestampOptions | undefined;
113
114
  constructor(name: string, options?: TimestampOptions);
115
+ fromDriverValue(value: unknown): Date;
116
+ toDriverValue(value: Date): string;
114
117
  toSqlType(): string;
115
118
  }
116
119
  export declare class UuidColumn extends PostgresqlColumn<string> {
@@ -166,6 +169,7 @@ export declare class PostgresqlDatabase<T extends Record<string, PostgresqlTable
166
169
  insert<T extends Table>(table: T): PostgresqlInsertStatement<T>;
167
170
  update<T extends Table>(table: T): PostgresqlUpdateStatement<T>;
168
171
  delete<T extends Table>(table: T): PostgresqlDeleteStatement<T>;
172
+ transaction<TResult>(run: (database: PostgresqlDatabase<T>) => Awaitable<TResult>): Promise<TResult>;
169
173
  }
170
174
  export declare function bigint(name: string): BigintColumn;
171
175
  export declare function bigserial(name: string): BigserialColumn;
@@ -159,6 +159,12 @@ var TimestampColumn = class extends PostgresqlColumn {
159
159
  super(name);
160
160
  this.$options = options;
161
161
  }
162
+ fromDriverValue(value) {
163
+ return value instanceof Date ? value : new Date(String(value).replace(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/, "$1T$2Z"));
164
+ }
165
+ toDriverValue(value) {
166
+ return value.toISOString().replace("T", " ").replace("Z", "");
167
+ }
162
168
  toSqlType() {
163
169
  return this.$options?.precision === void 0 ? "timestamp" : `timestamp(${this.$options.precision})`;
164
170
  }
@@ -245,7 +251,7 @@ var PostgresqlMigration = class extends Migration {
245
251
  return `"${value.replaceAll("\"", "\"\"")}"`;
246
252
  }
247
253
  };
248
- var PostgresqlDatabase = class extends Database {
254
+ var PostgresqlDatabase = class PostgresqlDatabase extends Database {
249
255
  $migration;
250
256
  constructor(session, schema) {
251
257
  super(session, schema);
@@ -263,6 +269,9 @@ var PostgresqlDatabase = class extends Database {
263
269
  delete(table) {
264
270
  return new PostgresqlDeleteStatement(this.$session, table);
265
271
  }
272
+ transaction(run) {
273
+ return this.$session.transaction((session) => run(new PostgresqlDatabase(session, this.$schema)));
274
+ }
266
275
  };
267
276
  function bigint(name) {
268
277
  return new BigintColumn(name);
@@ -1,4 +1,4 @@
1
- import type { Primitive } from "revojs/shared";
1
+ import type { Awaitable, Primitive } from "revojs/shared";
2
2
  import { CheckConstraint, Column, Database, DeleteStatement, ForeignKeyConstraint, Index, InsertStatement, Migration, PrimaryKeyConstraint, SelectStatement, Table, UniqueConstraint, UniqueIndex, UpdateStatement, type AppendToNullabilityMap, type AppendToSelection, type ColumnMeta, type Definition, type DeleteConstraintChange, type MigrationContext, type SelectMode, type Session, type Sql, type UpdateColumnChange } from "../index";
3
3
  export type IntegerMode = "number" | "timestamp" | "timestamp_ms" | "boolean";
4
4
  export type IntegerData<TMode> = TMode extends "timestamp" | "timestamp_ms" ? Date : TMode extends "boolean" ? boolean : number;
@@ -81,6 +81,7 @@ export declare class SqliteDatabase<T extends Record<string, SqliteTable>> exten
81
81
  insert<T extends Table>(table: T): SqliteInsertStatement<T>;
82
82
  update<T extends Table>(table: T): SqliteUpdateStatement<T>;
83
83
  delete<T extends Table>(table: T): SqliteDeleteStatement<T>;
84
+ transaction<TResult>(run: (database: SqliteDatabase<T>) => Awaitable<TResult>): Promise<TResult>;
84
85
  }
85
86
  export declare function blob(name: string): BlobColumn;
86
87
  export declare function integer<TMode extends IntegerMode = "number">(name: string, options?: IntegerOptions<TMode>): IntegerColumn<TMode>;
@@ -158,7 +158,7 @@ var SqliteMigration = class extends Migration {
158
158
  return super.serializeLiteral(value);
159
159
  }
160
160
  };
161
- var SqliteDatabase = class extends Database {
161
+ var SqliteDatabase = class SqliteDatabase extends Database {
162
162
  $migration;
163
163
  constructor(session, schema) {
164
164
  super(session, schema);
@@ -176,6 +176,9 @@ var SqliteDatabase = class extends Database {
176
176
  delete(table) {
177
177
  return new SqliteDeleteStatement(this.$session, table);
178
178
  }
179
+ transaction(run) {
180
+ return this.$session.transaction((session) => run(new SqliteDatabase(session, this.$schema)));
181
+ }
179
182
  };
180
183
  function blob(name) {
181
184
  return new BlobColumn(name);
@@ -1,18 +1,33 @@
1
1
  import type { Database, Statement } from "bun:sqlite";
2
- import type { Primitive } from "revojs/shared";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { Awaitable, Primitive } from "revojs/shared";
3
4
  import { PreparedQuery, Session, type Query } from "../index";
4
5
  import { SqliteDatabase, type SqliteTable } from "../protocols/sqlite";
6
+ interface BunSqliteTransaction {
7
+ active: boolean;
8
+ }
9
+ interface BunSqliteConnection {
10
+ context: AsyncLocalStorage<BunSqliteTransaction>;
11
+ queue: Promise<void>;
12
+ error?: Error;
13
+ }
5
14
  export declare class BunSqlitePreparedQuery extends PreparedQuery {
6
15
  protected $statement: Statement;
7
- constructor(query: Query, statement: Statement);
8
- run(parameters: Array<Primitive>): import("bun:sqlite").Changes;
9
- all(parameters: Array<Primitive>): (string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[][];
10
- get(parameters: Array<Primitive>): (string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[];
11
- values(parameters: Array<Primitive>): (string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[][];
16
+ protected $session: BunSqliteSession;
17
+ constructor(query: Query, statement: Statement, session: BunSqliteSession);
18
+ run(parameters: Array<Primitive>): Promise<import("bun:sqlite").Changes>;
19
+ all(parameters: Array<Primitive>): Promise<(string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[][]>;
20
+ get(parameters: Array<Primitive>): Promise<(string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[]>;
21
+ values(parameters: Array<Primitive>): Promise<(string | number | bigint | boolean | Uint8Array<ArrayBufferLike>)[][]>;
12
22
  }
13
23
  export declare class BunSqliteSession extends Session {
14
24
  protected $client: Database;
15
- constructor(client: Database);
25
+ protected $connection: BunSqliteConnection;
26
+ protected $transaction: BunSqliteTransaction | undefined;
27
+ constructor(client: Database, transaction?: BunSqliteTransaction);
28
+ execute<T>(run: () => Awaitable<T>): Promise<T>;
29
+ transaction<T>(run: (session: Session) => Awaitable<T>): Promise<T>;
16
30
  prepareQuery(query: Query): BunSqlitePreparedQuery;
17
31
  }
18
32
  export declare function bunSqlite<T extends Record<string, SqliteTable>>(client: Database, schema: T): SqliteDatabase<T>;
33
+ export {};
@@ -1,37 +1,94 @@
1
1
  import { PreparedQuery, Session } from "../index.js";
2
2
  import { SqliteDatabase } from "../protocols/sqlite.js";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
3
4
  //#region src/providers/bun-sqlite.ts
4
5
  var BunSqlitePreparedQuery = class extends PreparedQuery {
5
6
  $statement;
6
- constructor(query, statement) {
7
+ $session;
8
+ constructor(query, statement, session) {
7
9
  super(query);
8
10
  this.$statement = statement;
11
+ this.$session = session;
9
12
  }
10
13
  run(parameters) {
11
- return this.$statement.run(...parameters);
14
+ return this.$session.execute(() => this.$statement.run(...parameters));
12
15
  }
13
16
  all(parameters) {
14
- return this.$statement.values(...parameters);
17
+ return this.$session.execute(() => this.$statement.values(...parameters));
15
18
  }
16
19
  get(parameters) {
17
- return this.$statement.values(...parameters)[0];
20
+ return this.$session.execute(() => this.$statement.values(...parameters)[0]);
18
21
  }
19
22
  values(parameters) {
20
- return this.$statement.values(...parameters);
23
+ return this.$session.execute(() => this.$statement.values(...parameters));
21
24
  }
22
25
  };
23
- var BunSqliteSession = class extends Session {
26
+ var BunSqliteSession = class BunSqliteSession extends Session {
24
27
  $client;
25
- constructor(client) {
28
+ $connection;
29
+ $transaction;
30
+ constructor(client, transaction) {
26
31
  super();
27
32
  this.$client = client;
33
+ this.$transaction = transaction;
34
+ const connection = connections.get(client) ?? {
35
+ context: new AsyncLocalStorage(),
36
+ queue: Promise.resolve()
37
+ };
38
+ connections.set(client, connection);
39
+ this.$connection = connection;
40
+ }
41
+ async execute(run) {
42
+ if (this.$connection.error) throw this.$connection.error;
43
+ const transaction = this.$transaction ?? this.$connection.context.getStore();
44
+ if (transaction) {
45
+ if (!transaction.active) throw new Error("This transaction has already completed.");
46
+ return run();
47
+ }
48
+ const previous = this.$connection.queue;
49
+ const { promise, resolve } = Promise.withResolvers();
50
+ this.$connection.queue = promise;
51
+ await previous;
52
+ try {
53
+ if (this.$connection.error) throw this.$connection.error;
54
+ return await run();
55
+ } finally {
56
+ resolve();
57
+ }
58
+ }
59
+ async transaction(run) {
60
+ if (this.$transaction || this.$connection.context.getStore()) throw new Error("Nested Bun SQLite transactions are not supported.");
61
+ return this.execute(async () => {
62
+ if (this.$client.inTransaction) throw new Error("The Bun SQLite connection already has an externally managed transaction.");
63
+ this.$client.run("BEGIN IMMEDIATE");
64
+ const transaction = { active: true };
65
+ try {
66
+ const result = await this.$connection.context.run(transaction, () => run(new BunSqliteSession(this.$client, transaction)));
67
+ this.$client.run("COMMIT");
68
+ return result;
69
+ } catch (error) {
70
+ try {
71
+ this.$client.run("ROLLBACK");
72
+ } catch (rollbackError) {
73
+ this.$connection.error = new AggregateError([error, rollbackError], "Bun SQLite rollback failed; this connection can no longer be used safely.");
74
+ throw this.$connection.error;
75
+ }
76
+ throw error;
77
+ } finally {
78
+ transaction.active = false;
79
+ }
80
+ });
28
81
  }
29
82
  prepareQuery(query) {
30
- return new BunSqlitePreparedQuery(query, this.$client.prepare(query.sql));
83
+ if (this.$connection.error) throw this.$connection.error;
84
+ const transaction = this.$transaction ?? this.$connection.context.getStore();
85
+ if (transaction && !transaction.active) throw new Error("This transaction has already completed.");
86
+ return new BunSqlitePreparedQuery(query, this.$client.prepare(query.sql), this);
31
87
  }
32
88
  };
33
89
  function bunSqlite(client, schema) {
34
90
  return new SqliteDatabase(new BunSqliteSession(client), schema);
35
91
  }
92
+ var connections = /* @__PURE__ */ new WeakMap();
36
93
  //#endregion
37
94
  export { BunSqlitePreparedQuery, BunSqliteSession, bunSqlite };
@@ -1,5 +1,5 @@
1
1
  import type { D1Database, D1DatabaseSession, D1PreparedStatement } from "@cloudflare/workers-types";
2
- import type { Primitive } from "revojs/shared";
2
+ import type { Awaitable, Primitive } from "revojs/shared";
3
3
  import { PreparedQuery, Session, type Query } from "../index";
4
4
  import { SqliteDatabase, type SqliteTable } from "../protocols/sqlite";
5
5
  export declare class D1PreparedQuery extends PreparedQuery {
@@ -13,6 +13,7 @@ export declare class D1PreparedQuery extends PreparedQuery {
13
13
  export declare class D1Session extends Session {
14
14
  protected $client: D1Database | D1DatabaseSession;
15
15
  constructor(client: D1Database | D1DatabaseSession);
16
+ transaction<T>(run: (session: Session) => Awaitable<T>): Promise<T>;
16
17
  prepareQuery(query: Query): D1PreparedQuery;
17
18
  }
18
19
  export declare function d1<T extends Record<string, SqliteTable>>(client: D1Database | D1DatabaseSession, schema: T): SqliteDatabase<T>;
@@ -26,6 +26,9 @@ var D1Session = class extends Session {
26
26
  super();
27
27
  this.$client = client;
28
28
  }
29
+ async transaction(run) {
30
+ throw new Error("Cloudflare D1 does not support interactive transactions; use its atomic batch API instead.");
31
+ }
29
32
  prepareQuery(query) {
30
33
  return new D1PreparedQuery(query, this.$client.prepare(query.sql));
31
34
  }
@@ -1,14 +1,8 @@
1
- declare module "#config/storage" {
2
- import type { StorageConfig } from "@revojs/storage/module";
3
-
4
- const config: StorageConfig;
5
- export default config;
6
- }
7
-
8
1
  declare module "#storage" {
9
2
  import type { Database } from "../index";
10
- import type { Scope, Source } from "revojs";
3
+ import type { Resolvable, Source } from "revojs";
4
+ import type { Awaitable } from "revojs/shared";
11
5
 
12
- const entries: Source<(scope: Scope) => Database>;
6
+ const entries: Source<Resolvable<Awaitable<Database>>>;
13
7
  export default entries;
14
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revojs/storage",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "scripts": {
35
35
  "build": "vite build",
36
+ "typecheck": "tsc --noEmit -p tsconfig.json",
36
37
  "watch": "vite build --watch"
37
38
  },
38
39
  "dependencies": {
@@ -42,7 +43,7 @@
42
43
  "@revojs/tsconfig": "*"
43
44
  },
44
45
  "optionalDependencies": {
45
- "@cloudflare/workers-types": "^5.20261004.1",
46
+ "@cloudflare/workers-types": "^5.20261009.1",
46
47
  "@types/bun": "^1.4.2"
47
48
  }
48
49
  }