@c9up/atlas 0.1.3
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/LICENSE +21 -0
- package/README.md +35 -0
- package/db.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +69 -0
- package/scripts/copy-napi.mjs +86 -0
- package/src/AtlasProvider.ts +297 -0
- package/src/BaseEntity.ts +585 -0
- package/src/BaseRepository.ts +1694 -0
- package/src/ModelQuery.ts +2293 -0
- package/src/Transaction.ts +83 -0
- package/src/adapters/NapiDbAdapter.ts +178 -0
- package/src/config.ts +7 -0
- package/src/configure.ts +37 -0
- package/src/decorators/entity.ts +532 -0
- package/src/decorators/hooks.ts +169 -0
- package/src/decorators/scope.ts +44 -0
- package/src/errors.ts +111 -0
- package/src/index.ts +114 -0
- package/src/naming/NamingStrategy.ts +106 -0
- package/src/query/QueryBuilder.ts +422 -0
- package/src/query/native.ts +74 -0
- package/src/schema/Migration.ts +81 -0
- package/src/schema/MigrationRunner.ts +532 -0
- package/src/schema/Schema.ts +78 -0
- package/src/schema/SchemaBuilder.ts +14 -0
- package/src/schema/Seeder.ts +132 -0
- package/src/schema/TableBuilder.ts +238 -0
- package/src/schema/types.ts +51 -0
- package/src/services/db.ts +45 -0
- package/src/testing/DatabaseCleanup.ts +49 -0
- package/src/testing/Factory.ts +164 -0
- package/src/testing/TestDatabase.ts +81 -0
- package/src/testing/index.ts +3 -0
- package/src/utils/casing.ts +11 -0
- package/src/utils/dialectFromUrl.ts +16 -0
- package/src/utils/identifier.ts +35 -0
- package/src/utils/safePath.ts +59 -0
- package/src/utils/transactionBrand.ts +10 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDatabase — in-memory SQLite database for testing via the ream-db Rust driver.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DatabaseAdapter } from "../schema/MigrationRunner.js";
|
|
6
|
+
|
|
7
|
+
/** Simple async database interface for tests. */
|
|
8
|
+
export class Database {
|
|
9
|
+
#impl: DatabaseImpl;
|
|
10
|
+
|
|
11
|
+
constructor(impl: DatabaseImpl) {
|
|
12
|
+
this.#impl = impl;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Create an in-memory SQLite database backed by the Rust ream-db driver. */
|
|
16
|
+
static async memory(): Promise<Database> {
|
|
17
|
+
const { createNapiConnection } = await import(
|
|
18
|
+
"../adapters/NapiDbAdapter.js"
|
|
19
|
+
);
|
|
20
|
+
const conn = await createNapiConnection("sqlite::memory:", 1, 1);
|
|
21
|
+
return new Database({
|
|
22
|
+
async query(sql: string): Promise<Record<string, unknown>[]> {
|
|
23
|
+
return conn.query(sql);
|
|
24
|
+
},
|
|
25
|
+
async queryWithParams(
|
|
26
|
+
sql: string,
|
|
27
|
+
params: unknown[],
|
|
28
|
+
): Promise<Record<string, unknown>[]> {
|
|
29
|
+
return conn.query(sql, params);
|
|
30
|
+
},
|
|
31
|
+
async execute(sql: string, params?: unknown[]): Promise<void> {
|
|
32
|
+
await conn.execute(sql, params);
|
|
33
|
+
},
|
|
34
|
+
async close(): Promise<void> {
|
|
35
|
+
await conn.close();
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async query(sql: string): Promise<Record<string, unknown>[]> {
|
|
41
|
+
return this.#impl.query(sql);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async queryWithParams(
|
|
45
|
+
sql: string,
|
|
46
|
+
params: unknown[],
|
|
47
|
+
): Promise<Record<string, unknown>[]> {
|
|
48
|
+
return this.#impl.queryWithParams(sql, params);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async execute(sql: string, params?: unknown[]): Promise<void> {
|
|
52
|
+
return this.#impl.execute(sql, params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async close(): Promise<void> {
|
|
56
|
+
return this.#impl.close();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Get a DatabaseAdapter for MigrationRunner compatibility. */
|
|
60
|
+
asAdapter(): DatabaseAdapter {
|
|
61
|
+
return {
|
|
62
|
+
execute: (sql: string, params?: unknown[]) =>
|
|
63
|
+
this.#impl.execute(sql, params),
|
|
64
|
+
query: <T>(sql: string, params?: unknown[]) =>
|
|
65
|
+
params
|
|
66
|
+
? (this.#impl.queryWithParams(sql, params) as Promise<T[]>)
|
|
67
|
+
: (this.#impl.query(sql) as Promise<T[]>),
|
|
68
|
+
close: () => this.#impl.close(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DatabaseImpl {
|
|
74
|
+
query(sql: string): Promise<Record<string, unknown>[]>;
|
|
75
|
+
queryWithParams(
|
|
76
|
+
sql: string,
|
|
77
|
+
params: unknown[],
|
|
78
|
+
): Promise<Record<string, unknown>[]>;
|
|
79
|
+
execute(sql: string, params?: unknown[]): Promise<void>;
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Convert snake_case to camelCase. */
|
|
2
|
+
export function snakeToCamel(s: string): string {
|
|
3
|
+
return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Convert camelCase or PascalCase to snake_case (no leading underscore). */
|
|
7
|
+
export function camelToSnake(s: string): string {
|
|
8
|
+
return s.replace(/[A-Z]/g, (c, i) =>
|
|
9
|
+
i === 0 ? c.toLowerCase() : `_${c.toLowerCase()}`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for dialect detection. Both `AtlasProvider` and
|
|
3
|
+
* `NapiDbAdapter` used to ship their own copy — if a new driver scheme was
|
|
4
|
+
* added (e.g. `cockroachdb://`), only one side would be updated and the two
|
|
5
|
+
* would silently drift. Extracted here so every call site resolves the same
|
|
6
|
+
* way.
|
|
7
|
+
*/
|
|
8
|
+
import type { AtlasDialect } from "../query/native.js";
|
|
9
|
+
|
|
10
|
+
export function dialectFromUrl(url: string): AtlasDialect {
|
|
11
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://"))
|
|
12
|
+
return "postgres";
|
|
13
|
+
if (url.startsWith("mysql://") || url.startsWith("mariadb://"))
|
|
14
|
+
return "mysql";
|
|
15
|
+
return "sqlite";
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL identifier validation helpers — used by the TS-side fluent builders
|
|
3
|
+
* before a name is sent to the Rust compiler. Catches obvious garbage
|
|
4
|
+
* (empty / wrong chars / leading digit) early with a clear error site.
|
|
5
|
+
*
|
|
6
|
+
* The Rust compiler ALSO validates and quotes identifiers — these helpers
|
|
7
|
+
* are a defence-in-depth check, not the source of truth.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Pattern: starts with letter or underscore, then letters/digits/underscore/dot. */
|
|
11
|
+
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
12
|
+
|
|
13
|
+
/** Returns true if `name` is a syntactically valid SQL identifier (table, column, alias, CTE name, …). */
|
|
14
|
+
export function isValidIdentifier(name: unknown): name is string {
|
|
15
|
+
return (
|
|
16
|
+
typeof name === "string" && name.length > 0 && IDENTIFIER_RE.test(name)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Asserts that `name` is a valid identifier; throws a structured error otherwise.
|
|
22
|
+
*
|
|
23
|
+
* @param name the value to validate
|
|
24
|
+
* @param kind human-readable label used in the error message (e.g. "CTE name", "column name")
|
|
25
|
+
* @param errorFactory builds the error to throw — keeps this helper independent of any specific error class
|
|
26
|
+
*/
|
|
27
|
+
export function assertValidIdentifier(
|
|
28
|
+
name: unknown,
|
|
29
|
+
kind: string,
|
|
30
|
+
errorFactory: (message: string) => Error,
|
|
31
|
+
): asserts name is string {
|
|
32
|
+
if (!isValidIdentifier(name)) {
|
|
33
|
+
throw errorFactory(`${kind} must be a valid identifier: '${String(name)}'`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path/filename validation helpers shared by MigrationRunner and Seeder.
|
|
3
|
+
*
|
|
4
|
+
* Centralised here so the "forbidden chars + no `..`" rules + "path must stay
|
|
5
|
+
* inside its base directory" check live in one place. Previously duplicated
|
|
6
|
+
* — the audit flagged this as a maintenance hazard.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fsp from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { AtlasError } from "../errors.js";
|
|
12
|
+
|
|
13
|
+
/** Non-throwing async `fs.existsSync` equivalent. Returns false on ENOENT, propagates other errors. */
|
|
14
|
+
export async function pathExists(p: string): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
await fsp.access(p);
|
|
17
|
+
return true;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Throw if `name` contains characters that could escape a filename context
|
|
26
|
+
* (path separators, quotes, backticks) or include a directory-walk sequence
|
|
27
|
+
* (`..`). Does NOT check extension or existence — that's the caller's job.
|
|
28
|
+
*/
|
|
29
|
+
export function assertSafeName(
|
|
30
|
+
name: string,
|
|
31
|
+
errorCode: string,
|
|
32
|
+
kind: string,
|
|
33
|
+
): void {
|
|
34
|
+
if (/[/\\'";`]/.test(name) || name.includes("..")) {
|
|
35
|
+
throw new AtlasError(errorCode, `Invalid ${kind} name: ${name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `fileName` inside `baseDir` and throw if the resulting path escapes
|
|
41
|
+
* the base — guards against symlink / `../` traversal attacks when loading
|
|
42
|
+
* migration or seeder files dynamically.
|
|
43
|
+
*/
|
|
44
|
+
export function assertPathInsideBase(
|
|
45
|
+
baseDir: string,
|
|
46
|
+
fileName: string,
|
|
47
|
+
errorCode: string,
|
|
48
|
+
kind: string,
|
|
49
|
+
): string {
|
|
50
|
+
const resolved = path.resolve(baseDir, fileName);
|
|
51
|
+
const base = path.resolve(baseDir);
|
|
52
|
+
if (!resolved.startsWith(base + path.sep) && resolved !== base) {
|
|
53
|
+
throw new AtlasError(
|
|
54
|
+
errorCode,
|
|
55
|
+
`${kind} path escapes directory: ${fileName}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DatabaseConnection } from "../BaseRepository.js";
|
|
2
|
+
import type { TransactionClient } from "../Transaction.js";
|
|
3
|
+
|
|
4
|
+
export const TRANSACTION_BRAND = Symbol.for("atlas:transaction");
|
|
5
|
+
|
|
6
|
+
export function isTransactionClient(
|
|
7
|
+
db: DatabaseConnection,
|
|
8
|
+
): db is TransactionClient {
|
|
9
|
+
return TRANSACTION_BRAND in db;
|
|
10
|
+
}
|