arkormx 2.0.0-next.0 → 2.0.0-next.2

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.
package/README.md CHANGED
@@ -7,12 +7,12 @@
7
7
  [![Deploy Documentation](https://github.com/arkstack-hq/arkormx/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/arkstack-hq/arkormx/actions/workflows/deploy-docs.yml)
8
8
  [![codecov](https://codecov.io/gh/arkstack-hq/arkormx/graph/badge.svg?token=ls1VVoFkYh)](https://codecov.io/gh/arkstack-hq/arkormx)
9
9
 
10
- Arkormˣ is a framework-agnostic ORM designed to run anywhere Node.js runs. It brings a familiar model layer and fluent query builder on top of Prisma delegates, enabling clean, modern, and type-safe development.
10
+ Arkormˣ is a framework-agnostic ORM designed to run anywhere Node.js runs. It brings a familiar model layer and fluent query builder on top of adapter-backed execution, with Prisma compatibility kept during the current transition window.
11
11
 
12
12
  ## Features
13
13
 
14
- - Arkormˣ is built on top of Prisma, providing a familiar and powerful API for database interactions.
15
- - Delegate-backed query execution with practical ORM ergonomics.
14
+ - Adapter-backed query execution with practical ORM ergonomics.
15
+ - Adapter-first runtime setup with Kysely/Postgres support and a Prisma compatibility adapter during migration.
16
16
  - End-to-end guides for setup, querying, relationships, migrations, and CLI usage.
17
17
  - Full TypeScript support, providing strong typing and improved developer experience.
18
18
  - Follows best practices for security, ensuring your data is protected.
@@ -26,27 +26,49 @@ Arkormˣ is a framework-agnostic ORM designed to run anywhere Node.js runs. It b
26
26
  Stable release:
27
27
 
28
28
  ```sh
29
- pnpm add arkormx @prisma/client
30
- pnpm add -D prisma
29
+ pnpm add arkormx kysely pg
31
30
  ```
32
31
 
33
32
  Preview release (`next`):
34
33
 
35
34
  ```sh
36
- pnpm add arkormx@next @prisma/client
37
- pnpm add -D prisma
35
+ pnpm add arkormx@next kysely pg
38
36
  ```
39
37
 
40
38
  ### Configuration
41
39
 
40
+ Primary runtime path:
41
+
42
+ ```ts
43
+ import { createKyselyAdapter, defineConfig } from 'arkormx';
44
+ import { Kysely, PostgresDialect } from 'kysely';
45
+ import { Pool } from 'pg';
46
+
47
+ export default defineConfig({
48
+ adapter: createKyselyAdapter(
49
+ new Kysely<Record<string, never>>({
50
+ dialect: new PostgresDialect({
51
+ pool: new Pool({
52
+ connectionString: process.env.DATABASE_URL,
53
+ }),
54
+ }),
55
+ }),
56
+ ),
57
+ });
58
+ ```
59
+
60
+ Optional compatibility/runtime config for CLI and transaction helpers:
61
+
42
62
  Create `arkormx.config.js` in your project root:
43
63
 
44
64
  ```ts
45
65
  import { defineConfig } from 'arkormx';
46
66
  import { PrismaClient } from '@prisma/client';
47
67
 
68
+ const prisma = new PrismaClient();
69
+
48
70
  export default defineConfig({
49
- prisma: new PrismaClient(...),
71
+ prisma: () => prisma,
50
72
  });
51
73
  ```
52
74
 
@@ -57,15 +79,21 @@ Or run the Arkormˣ CLI command `npx arkorm init` to initialize your project alo
57
79
  ```ts
58
80
  import { Model } from 'arkormx';
59
81
 
60
- export class User extends Model<'users'> {
61
- protected static override delegate = 'users'; // not required if your model name matches the delegate name or the pluralized form of it
62
- }
82
+ type UserAttributes = {
83
+ id: number;
84
+ email: string;
85
+ name: string;
86
+ isActive: boolean;
87
+ };
88
+
89
+ export class User extends Model<UserAttributes> {}
63
90
  ```
64
91
 
65
- ### Generate Prisma client
92
+ ### Optional Prisma compatibility
66
93
 
67
94
  ```sh
68
- pnpm prisma generate
95
+ pnpm add @prisma/client
96
+ pnpm add -D prisma
69
97
  ```
70
98
 
71
99
  ### Run queries
@@ -98,6 +126,7 @@ await User.transaction(async () => {
98
126
 
99
127
  - [Setup](https://arkormx.toneflix.net/guide/setup)
100
128
  - [Configuration](https://arkormx.dev/guide/configuration)
129
+ - [Prisma Compatibility](https://arkormx.dev/guide/prisma-compatibility)
101
130
  - [Typing](https://arkormx.dev/guide/typing)
102
131
  - [Models](https://arkormx.dev/guide/models)
103
132
  - [Query Builder](https://arkormx.dev/guide/query-builder)
package/dist/cli.mjs CHANGED
@@ -1458,6 +1458,7 @@ const userConfig = {
1458
1458
  let runtimeConfigLoaded = false;
1459
1459
  let runtimeConfigLoadingPromise;
1460
1460
  let runtimeClientResolver;
1461
+ let runtimeAdapter;
1461
1462
  let runtimePaginationURLDriverFactory;
1462
1463
  let runtimePaginationCurrentPageResolver;
1463
1464
  const transactionClientStorage = new AsyncLocalStorage();
@@ -1474,6 +1475,12 @@ const mergePathConfig = (paths) => {
1474
1475
  ...incoming
1475
1476
  };
1476
1477
  };
1478
+ const bindAdapterToModels = (adapter, models) => {
1479
+ models.forEach((model) => {
1480
+ model.setAdapter(adapter);
1481
+ });
1482
+ return adapter;
1483
+ };
1477
1484
  /**
1478
1485
  * Get the user-provided ArkORM configuration.
1479
1486
  *
@@ -1493,15 +1500,35 @@ const getUserConfig = (key) => {
1493
1500
  const configureArkormRuntime = (prisma, options = {}) => {
1494
1501
  const nextConfig = {
1495
1502
  ...userConfig,
1496
- prisma,
1497
1503
  paths: mergePathConfig(options.paths)
1498
1504
  };
1505
+ nextConfig.prisma = prisma;
1499
1506
  if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
1507
+ if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
1508
+ if (options.boot !== void 0) nextConfig.boot = options.boot;
1500
1509
  if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
1501
1510
  Object.assign(userConfig, { ...nextConfig });
1502
1511
  runtimeClientResolver = prisma;
1512
+ runtimeAdapter = options.adapter;
1503
1513
  runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
1504
1514
  runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
1515
+ options.boot?.({
1516
+ prisma: resolveClient(prisma),
1517
+ bindAdapter: bindAdapterToModels
1518
+ });
1519
+ };
1520
+ /**
1521
+ * Resolve a Prisma client instance from the provided resolver, which can be either
1522
+ * a direct client instance or a function that returns a client instance.
1523
+ *
1524
+ * @param resolver
1525
+ * @returns
1526
+ */
1527
+ const resolveClient = (resolver) => {
1528
+ if (!resolver) return void 0;
1529
+ const client = typeof resolver === "function" ? resolver() : resolver;
1530
+ if (!client || typeof client !== "object") return void 0;
1531
+ return client;
1505
1532
  };
1506
1533
  /**
1507
1534
  * Resolve and apply the ArkORM configuration from an imported module.
@@ -1513,8 +1540,10 @@ const configureArkormRuntime = (prisma, options = {}) => {
1513
1540
  */
1514
1541
  const resolveAndApplyConfig = (imported) => {
1515
1542
  const config = imported?.default ?? imported;
1516
- if (!config || typeof config !== "object" || !config.prisma) return;
1543
+ if (!config || typeof config !== "object") return;
1517
1544
  configureArkormRuntime(config.prisma, {
1545
+ adapter: config.adapter,
1546
+ boot: config.boot,
1518
1547
  pagination: config.pagination,
1519
1548
  paths: config.paths,
1520
1549
  outputExt: config.outputExt
@@ -2072,6 +2101,40 @@ var CliApp = class {
2072
2101
  lines.splice(insertionIndex, 0, `import type { ${enumTypes.join(", ")} } from '@prisma/client'`);
2073
2102
  return lines.join("\n");
2074
2103
  }
2104
+ parseModelSyncSource(modelSource) {
2105
+ const classMatch = modelSource.match(/export\s+class\s+(\w+)\s+extends\s+Model(?:<[^\n]+>)?\s*\{/);
2106
+ if (!classMatch) return null;
2107
+ const className = classMatch[1];
2108
+ const tableMatch = modelSource.match(/protected\s+static\s+override\s+table\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+table\s*=\s*['"]([^'"]+)['"]/);
2109
+ const delegateMatch = modelSource.match(/protected\s+static\s+override\s+delegate\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+delegate\s*=\s*['"]([^'"]+)['"]/);
2110
+ return {
2111
+ className,
2112
+ table: tableMatch?.[1] ?? delegateMatch?.[1] ?? str(className).camel().plural().toString()
2113
+ };
2114
+ }
2115
+ syncModelFiles(modelFiles, resolveStructure, enums) {
2116
+ const updated = [];
2117
+ const skipped = [];
2118
+ modelFiles.forEach((filePath) => {
2119
+ const source = readFileSync$1(filePath, "utf-8");
2120
+ const structure = resolveStructure(filePath, source);
2121
+ if (!structure || structure.fields.length === 0) {
2122
+ skipped.push(filePath);
2123
+ return;
2124
+ }
2125
+ const synced = this.syncModelDeclarations(source, structure.fields, enums);
2126
+ if (!synced.updated) {
2127
+ skipped.push(filePath);
2128
+ return;
2129
+ }
2130
+ writeFileSync$1(filePath, synced.content);
2131
+ updated.push(filePath);
2132
+ });
2133
+ return {
2134
+ updated,
2135
+ skipped
2136
+ };
2137
+ }
2075
2138
  /**
2076
2139
  * Parse Prisma enum definitions from a schema and return their member names.
2077
2140
  *
@@ -2164,7 +2227,7 @@ var CliApp = class {
2164
2227
  */
2165
2228
  syncModelDeclarations(modelSource, declarations, enums) {
2166
2229
  const lines = modelSource.split("\n");
2167
- const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
2230
+ const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model(?:<[^\n]+>)?\s*\{/.test(line));
2168
2231
  if (classIndex < 0) return {
2169
2232
  content: modelSource,
2170
2233
  updated: false
@@ -2218,6 +2281,36 @@ var CliApp = class {
2218
2281
  updated: contentWithImports !== modelSource
2219
2282
  };
2220
2283
  }
2284
+ async syncModels(options = {}) {
2285
+ const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", join$1(process.cwd(), "src", "models"));
2286
+ if (!existsSync$1(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
2287
+ const modelFiles = readdirSync$1(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => join$1(modelsDir, file));
2288
+ const adapter = this.getConfig("adapter");
2289
+ if (adapter && typeof adapter.introspectModels === "function") {
2290
+ const sources = modelFiles.reduce((all, filePath) => {
2291
+ const parsed = this.parseModelSyncSource(readFileSync$1(filePath, "utf-8"));
2292
+ if (parsed) all.set(filePath, parsed);
2293
+ return all;
2294
+ }, /* @__PURE__ */ new Map());
2295
+ const discovered = await adapter.introspectModels({ tables: [...new Set([...sources.values()].map((source) => source.table))] });
2296
+ const structuresByTable = new Map(discovered.map((model) => [model.table, model]));
2297
+ const result = this.syncModelFiles(modelFiles, (filePath) => {
2298
+ const parsed = sources.get(filePath);
2299
+ return parsed ? structuresByTable.get(parsed.table) : void 0;
2300
+ }, /* @__PURE__ */ new Map());
2301
+ return {
2302
+ source: "adapter",
2303
+ modelsDir,
2304
+ total: modelFiles.length,
2305
+ updated: result.updated,
2306
+ skipped: result.skipped
2307
+ };
2308
+ }
2309
+ return {
2310
+ source: "prisma",
2311
+ ...this.syncModelsFromPrisma(options)
2312
+ };
2313
+ }
2221
2314
  /**
2222
2315
  * Sync model attribute declarations in model files based on the Prisma schema.
2223
2316
  * This method reads the Prisma schema to extract model definitions and their
@@ -2237,38 +2330,18 @@ var CliApp = class {
2237
2330
  const schema = readFileSync$1(schemaPath, "utf-8");
2238
2331
  const prismaEnums = this.parsePrismaEnums(schema);
2239
2332
  const prismaModels = this.parsePrismaModels(schema);
2240
- const modelFiles = readdirSync$1(modelsDir).filter((file) => file.endsWith(".ts"));
2241
- const updated = [];
2242
- const skipped = [];
2243
- modelFiles.forEach((file) => {
2244
- const filePath = join$1(modelsDir, file);
2245
- const source = readFileSync$1(filePath, "utf-8");
2246
- const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
2247
- if (!classMatch) {
2248
- skipped.push(filePath);
2249
- return;
2250
- }
2251
- const className = classMatch[1];
2252
- const delegate = classMatch[2];
2253
- const prismaModel = prismaModels.find((model) => model.table === delegate) ?? prismaModels.find((model) => model.name === className);
2254
- if (!prismaModel || prismaModel.fields.length === 0) {
2255
- skipped.push(filePath);
2256
- return;
2257
- }
2258
- const synced = this.syncModelDeclarations(source, prismaModel.fields, prismaEnums);
2259
- if (!synced.updated) {
2260
- skipped.push(filePath);
2261
- return;
2262
- }
2263
- writeFileSync$1(filePath, synced.content);
2264
- updated.push(filePath);
2265
- });
2333
+ const modelFiles = readdirSync$1(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => join$1(modelsDir, file));
2334
+ const result = this.syncModelFiles(modelFiles, (filePath, source) => {
2335
+ const parsed = this.parseModelSyncSource(source);
2336
+ if (!parsed) return void 0;
2337
+ return prismaModels.find((model) => model.table === parsed.table) ?? prismaModels.find((model) => model.name === parsed.className);
2338
+ }, prismaEnums);
2266
2339
  return {
2267
2340
  schemaPath,
2268
2341
  modelsDir,
2269
2342
  total: modelFiles.length,
2270
- updated,
2271
- skipped
2343
+ updated: result.updated,
2344
+ skipped: result.skipped
2272
2345
  };
2273
2346
  }
2274
2347
  };
@@ -2846,20 +2919,21 @@ var MigrationHistoryCommand = class extends Command {
2846
2919
  //#region src/cli/commands/ModelsSyncCommand.ts
2847
2920
  var ModelsSyncCommand = class extends Command {
2848
2921
  signature = `models:sync
2849
- {--schema= : Path to prisma schema file}
2922
+ {--schema= : Path to prisma schema file used when adapter introspection is unavailable}
2850
2923
  {--models= : Path to models directory}
2851
2924
  `;
2852
- description = "Sync model declare attributes from prisma schema for all model files";
2925
+ description = "Sync model declare attributes from the active adapter when supported, otherwise fall back to the Prisma schema";
2853
2926
  async handle() {
2854
2927
  this.app.command = this;
2855
- const result = this.app.syncModelsFromPrisma({
2928
+ const result = await this.app.syncModels({
2856
2929
  schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
2857
2930
  modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
2858
2931
  });
2859
2932
  const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
2860
2933
  this.success("SUCCESS: Model sync completed with the following results:");
2861
2934
  [
2862
- this.app.splitLogger("Schema", result.schemaPath),
2935
+ this.app.splitLogger("Source", result.source === "adapter" ? "adapter introspection" : "prisma schema"),
2936
+ ...result.schemaPath ? [this.app.splitLogger("Schema", result.schemaPath)] : [],
2863
2937
  this.app.splitLogger("Models", result.modelsDir),
2864
2938
  this.app.splitLogger("Processed", String(result.total)),
2865
2939
  ...updatedLines,