accio-orm 0.1.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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -0
  3. package/dist/connection/Connection.d.ts +29 -0
  4. package/dist/connection/Connection.js +63 -0
  5. package/dist/connection/types.d.ts +7 -0
  6. package/dist/connection/types.js +2 -0
  7. package/dist/decorators/Column.d.ts +23 -0
  8. package/dist/decorators/Column.js +34 -0
  9. package/dist/decorators/PrimaryColumn.d.ts +9 -0
  10. package/dist/decorators/PrimaryColumn.js +26 -0
  11. package/dist/decorators/Table.d.ts +10 -0
  12. package/dist/decorators/Table.js +21 -0
  13. package/dist/examples/test-connection.d.ts +1 -0
  14. package/dist/examples/test-connection.js +36 -0
  15. package/dist/examples/test-decorators.d.ts +1 -0
  16. package/dist/examples/test-decorators.js +40 -0
  17. package/dist/examples/test-metadata.d.ts +1 -0
  18. package/dist/examples/test-metadata.js +110 -0
  19. package/dist/examples/test-querybuilder.d.ts +1 -0
  20. package/dist/examples/test-querybuilder.js +142 -0
  21. package/dist/examples/test-repository.d.ts +1 -0
  22. package/dist/examples/test-repository.js +111 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.js +23 -0
  25. package/dist/metadata/MetadataStorage.d.ts +30 -0
  26. package/dist/metadata/MetadataStorage.js +82 -0
  27. package/dist/metadata/types.d.ts +7 -0
  28. package/dist/metadata/types.js +2 -0
  29. package/dist/query/QueryBuilder.d.ts +64 -0
  30. package/dist/query/QueryBuilder.js +180 -0
  31. package/dist/repository/Repository.d.ts +43 -0
  32. package/dist/repository/Repository.js +156 -0
  33. package/dist/utils/entityMapper.d.ts +9 -0
  34. package/dist/utils/entityMapper.js +22 -0
  35. package/package.json +72 -0
@@ -0,0 +1,64 @@
1
+ import type { Connection } from '../connection/Connection';
2
+ import type { EntityMetadata } from '../metadata/types';
3
+ import type { Repository } from '../repository/Repository';
4
+ export declare class QueryBuilder<T> {
5
+ private repository;
6
+ private connection;
7
+ private metadata;
8
+ private conditions;
9
+ private limitValue?;
10
+ private offsetValue?;
11
+ private orderByColumn?;
12
+ private orderDirection;
13
+ constructor(repository: Repository<T>, connection: Connection, metadata: EntityMetadata, initialConditions?: Partial<T>);
14
+ /**
15
+ * Map a database row to an entity instance
16
+ * @private
17
+ */
18
+ private mapRowToEntity;
19
+ /**
20
+ * Add WHERE conditions (can be chained multiple times)
21
+ * Multiple calls to where() are combined with AND
22
+ */
23
+ where(conditions: Partial<T>): this;
24
+ /**
25
+ * Set the maximum number of results to return
26
+ */
27
+ limit(value: number): this;
28
+ /**
29
+ * Set the number of results to skip
30
+ */
31
+ offset(value: number): this;
32
+ /**
33
+ * Order results by a column
34
+ */
35
+ orderBy(column: string, direction?: 'ASC' | 'DESC'): this;
36
+ /**
37
+ * Execute the query and return all matching results
38
+ */
39
+ find(): Promise<T[]>;
40
+ /**
41
+ * Execute the query and return the first result (or null)
42
+ */
43
+ findOne(): Promise<T | null>;
44
+ /**
45
+ * Count the number of results that match the query
46
+ */
47
+ count(): Promise<number>;
48
+ /**
49
+ * Check if any results exist matching the query
50
+ */
51
+ exists(): Promise<boolean>;
52
+ /**
53
+ * Build the SQL query and parameters
54
+ * @private
55
+ */
56
+ private buildSQL;
57
+ /**
58
+ * Get the SQL query that would be executed (for debugging)
59
+ */
60
+ toSQL(): {
61
+ sql: string;
62
+ params: unknown[];
63
+ };
64
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryBuilder = void 0;
4
+ const entityMapper_1 = require("../utils/entityMapper");
5
+ class QueryBuilder {
6
+ constructor(repository, connection, metadata, initialConditions) {
7
+ this.conditions = [];
8
+ this.orderDirection = 'ASC';
9
+ this.repository = repository;
10
+ this.connection = connection;
11
+ this.metadata = metadata;
12
+ if (initialConditions) {
13
+ this.conditions.push(initialConditions);
14
+ }
15
+ }
16
+ /**
17
+ * Map a database row to an entity instance
18
+ * @private
19
+ */
20
+ mapRowToEntity(row) {
21
+ // Get the entity class from the repository
22
+ const entityClass = this.repository.getEntityClass();
23
+ const entity = new entityClass();
24
+ // Map each column from the database row to the entity property
25
+ this.metadata.columns.forEach((col) => {
26
+ const value = row[col.columnName];
27
+ entity[col.propertyKey] = value;
28
+ });
29
+ return entity;
30
+ }
31
+ /**
32
+ * Add WHERE conditions (can be chained multiple times)
33
+ * Multiple calls to where() are combined with AND
34
+ */
35
+ where(conditions) {
36
+ this.conditions.push(conditions);
37
+ return this;
38
+ }
39
+ /**
40
+ * Set the maximum number of results to return
41
+ */
42
+ limit(value) {
43
+ if (value < 0) {
44
+ throw new Error('Limit must be a positive number');
45
+ }
46
+ this.limitValue = value;
47
+ return this;
48
+ }
49
+ /**
50
+ * Set the number of results to skip
51
+ */
52
+ offset(value) {
53
+ if (value < 0) {
54
+ throw new Error('Offset must be a positive number');
55
+ }
56
+ this.offsetValue = value;
57
+ return this;
58
+ }
59
+ /**
60
+ * Order results by a column
61
+ */
62
+ orderBy(column, direction = 'ASC') {
63
+ // Validate that the column exists
64
+ const columnMeta = this.metadata.columns.find((col) => col.propertyKey === column);
65
+ if (!columnMeta) {
66
+ throw new Error(`Column '${column}' does not exist on entity. ` +
67
+ `Available columns: ${this.metadata.columns.map((c) => c.propertyKey).join(', ')}`);
68
+ }
69
+ this.orderByColumn = columnMeta.columnName;
70
+ this.orderDirection = direction;
71
+ return this;
72
+ }
73
+ /**
74
+ * Execute the query and return all matching results
75
+ */
76
+ async find() {
77
+ const { sql, params } = this.buildSQL();
78
+ const result = await this.connection.query(sql, params);
79
+ // Get entity class from repository
80
+ const entityClass = this.repository.getEntityClass();
81
+ return (0, entityMapper_1.mapRowsToEntities)(result.rows, entityClass, this.metadata);
82
+ }
83
+ /**
84
+ * Execute the query and return the first result (or null)
85
+ */
86
+ async findOne() {
87
+ // Automatically add LIMIT 1 for performance
88
+ this.limit(1);
89
+ const results = await this.find();
90
+ return results[0] || null;
91
+ }
92
+ /**
93
+ * Count the number of results that match the query
94
+ */
95
+ async count() {
96
+ const { sql: baseSQL, params } = this.buildSQL(true); // true = count mode
97
+ // Replace SELECT * with SELECT COUNT(*)
98
+ const sql = baseSQL.replace(`SELECT * FROM ${this.metadata.tableName}`, `SELECT COUNT(*) as count FROM ${this.metadata.tableName}`);
99
+ const result = await this.connection.query(sql, params);
100
+ const row = result.rows[0];
101
+ return parseInt(String(row.count), 10);
102
+ }
103
+ /**
104
+ * Check if any results exist matching the query
105
+ */
106
+ async exists() {
107
+ const count = await this.count();
108
+ return count > 0;
109
+ }
110
+ /**
111
+ * Build the SQL query and parameters
112
+ * @private
113
+ */
114
+ buildSQL(skipLimitOffset = false) {
115
+ let sql = `SELECT * FROM ${this.metadata.tableName}`;
116
+ const params = [];
117
+ // build where clause;
118
+ if (this.conditions.length > 0) {
119
+ const whereClauses = [];
120
+ this.conditions.forEach((condition) => {
121
+ Object.entries(condition).forEach(([key, value]) => {
122
+ // find column metadata for this property
123
+ const column = this.metadata.columns.find((col) => col.propertyKey === key);
124
+ if (!column) {
125
+ throw new Error(`Property '${key}' does not exist on entity ${this.metadata.tableName}. ` +
126
+ `Available properties: ${this.metadata.columns.map((c) => c.propertyKey).join(', ')}`);
127
+ }
128
+ // Hanfle different value types
129
+ if (value === null) {
130
+ whereClauses.push(`${column.columnName} IS NULL`);
131
+ }
132
+ else if (Array.isArray(value)) {
133
+ // IN clause: WHERE column IN ($1, $2, $3)
134
+ if (value.length === 0) {
135
+ whereClauses.push('1 = 0');
136
+ }
137
+ else {
138
+ const placeholders = value
139
+ .map((v) => {
140
+ params.push(v);
141
+ return `$${params.length}`;
142
+ })
143
+ .join(', ');
144
+ whereClauses.push(`${column.columnName} IN (${placeholders})`);
145
+ }
146
+ }
147
+ else {
148
+ // regular equality: WHERE column = $1
149
+ params.push(value);
150
+ whereClauses.push(`${column.columnName} = $${params.length}`);
151
+ }
152
+ });
153
+ });
154
+ if (whereClauses.length > 0) {
155
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
156
+ }
157
+ // Add ORDER BY
158
+ if (this.orderByColumn) {
159
+ sql += ` ORDER BY ${this.orderByColumn} ${this.orderDirection}`;
160
+ }
161
+ // Add LIMIT and OFFSET (skip for count queries)
162
+ if (!skipLimitOffset) {
163
+ if (this.limitValue !== undefined) {
164
+ sql += ` LIMIT ${this.limitValue}`;
165
+ }
166
+ if (this.offsetValue !== undefined) {
167
+ sql += ` OFFSET ${this.offsetValue}`;
168
+ }
169
+ }
170
+ }
171
+ return { sql, params };
172
+ }
173
+ /**
174
+ * Get the SQL query that would be executed (for debugging)
175
+ */
176
+ toSQL() {
177
+ return this.buildSQL();
178
+ }
179
+ }
180
+ exports.QueryBuilder = QueryBuilder;
@@ -0,0 +1,43 @@
1
+ import type { Connection } from '@/connection/Connection';
2
+ import type { EntityConstructor, EntityMetadata } from '@/metadata/types';
3
+ import { QueryBuilder } from '@/query/QueryBuilder';
4
+ export declare class Repository<T> {
5
+ private entityClass;
6
+ private connection;
7
+ private metadata;
8
+ constructor(entityClass: EntityConstructor, connection: Connection);
9
+ getConnection(): Connection;
10
+ getMetadata(): EntityMetadata;
11
+ getEntityClass(): EntityConstructor;
12
+ where(conditions: Partial<T>): QueryBuilder<T>;
13
+ findById(id: unknown): Promise<T | null>;
14
+ /**
15
+ * Find all entities
16
+ */
17
+ findAll(): Promise<T[]>;
18
+ save(entity: T): Promise<T>;
19
+ /**
20
+ * Insert a new entity
21
+ */
22
+ insert(entity: T): Promise<T>;
23
+ /**
24
+ * Update an existing entity
25
+ */
26
+ update(entity: T): Promise<T>;
27
+ /**
28
+ * Delete an entity
29
+ */
30
+ delete(entity: T): Promise<void>;
31
+ /**
32
+ * Delete by primary key
33
+ */
34
+ deleteById(id: unknown): Promise<void>;
35
+ /**
36
+ * Count all entities
37
+ */
38
+ count(): Promise<number>;
39
+ /**
40
+ * Check if entity exists by primary key
41
+ */
42
+ exists(id: unknown): Promise<boolean>;
43
+ }
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Repository = void 0;
4
+ const MetadataStorage_1 = require("@/metadata/MetadataStorage");
5
+ const QueryBuilder_1 = require("@/query/QueryBuilder");
6
+ const entityMapper_1 = require("../utils/entityMapper");
7
+ class Repository {
8
+ constructor(entityClass, connection) {
9
+ this.entityClass = entityClass;
10
+ this.connection = connection;
11
+ // extract and cache metadata on construction
12
+ this.metadata = MetadataStorage_1.MetadataStorage.getEntityMetadata(entityClass);
13
+ }
14
+ getConnection() {
15
+ return this.connection;
16
+ }
17
+ getMetadata() {
18
+ return this.metadata;
19
+ }
20
+ getEntityClass() {
21
+ return this.entityClass;
22
+ }
23
+ where(conditions) {
24
+ return new QueryBuilder_1.QueryBuilder(this, this.connection, this.metadata, conditions);
25
+ }
26
+ async findById(id) {
27
+ if (!this.metadata.primaryColumn) {
28
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
29
+ }
30
+ const primaryKey = this.metadata.primaryColumn.columnName;
31
+ const sql = `SELECT * FROM ${this.metadata.tableName} WHERE ${primaryKey} = $1`;
32
+ const result = await this.connection.query(sql, [id]);
33
+ if (result.rows.length === 0) {
34
+ return null;
35
+ }
36
+ return (0, entityMapper_1.mapRowToEntity)(result.rows[0], this.entityClass, this.metadata);
37
+ }
38
+ /**
39
+ * Find all entities
40
+ */
41
+ async findAll() {
42
+ const sql = `SELECT * FROM ${this.metadata.tableName}`;
43
+ const result = await this.connection.query(sql);
44
+ return (0, entityMapper_1.mapRowsToEntities)(result.rows, this.entityClass, this.metadata);
45
+ }
46
+ save(entity) {
47
+ if (!this.metadata.primaryColumn) {
48
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
49
+ }
50
+ const primaryKey = this.metadata.primaryColumn.columnName;
51
+ const id = entity[primaryKey];
52
+ if (id !== undefined && id !== null) {
53
+ return this.update(entity);
54
+ }
55
+ else {
56
+ return this.insert(entity);
57
+ }
58
+ }
59
+ /**
60
+ * Insert a new entity
61
+ */
62
+ async insert(entity) {
63
+ const columns = this.metadata.columns.filter((col) => !col.isPrimary);
64
+ if (columns.length === 0) {
65
+ throw new Error(`Entity ${this.entityClass.name} has no columns to insert (only primary key)`);
66
+ }
67
+ const columnNames = columns.map((col) => col.columnName).join(', ');
68
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
69
+ const values = columns.map((col) => entity[col.propertyKey]);
70
+ const sql = `
71
+ INSERT INTO ${this.metadata.tableName} (${columnNames})
72
+ VALUES (${placeholders})
73
+ RETURNING *
74
+ `;
75
+ const result = await this.connection.query(sql, values);
76
+ return (0, entityMapper_1.mapRowToEntity)(result.rows[0], this.entityClass, this.metadata);
77
+ }
78
+ /**
79
+ * Update an existing entity
80
+ */
81
+ async update(entity) {
82
+ if (!this.metadata.primaryColumn) {
83
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
84
+ }
85
+ const columns = this.metadata.columns.filter((col) => !col.isPrimary);
86
+ if (columns.length === 0) {
87
+ throw new Error(`Entity ${this.entityClass.name} has no columns to update (only primary key)`);
88
+ }
89
+ const primaryKey = this.metadata.primaryColumn.columnName;
90
+ const primaryValue = entity[this.metadata.primaryColumn.propertyKey];
91
+ // Build SET clause: column1 = $1, column2 = $2, ...
92
+ const setClause = columns
93
+ .map((col, i) => `${col.columnName} = $${i + 1}`)
94
+ .join(', ');
95
+ const values = columns.map((col) => entity[col.propertyKey]);
96
+ const sql = `
97
+ UPDATE ${this.metadata.tableName}
98
+ SET ${setClause}
99
+ WHERE ${primaryKey} = $${columns.length + 1}
100
+ RETURNING *
101
+ `;
102
+ const result = await this.connection.query(sql, [...values, primaryValue]);
103
+ if (result.rows.length === 0) {
104
+ throw new Error(`Update failed: Entity with ${primaryKey} = ${String(primaryValue)} not found`);
105
+ }
106
+ return (0, entityMapper_1.mapRowToEntity)(result.rows[0], this.entityClass, this.metadata);
107
+ }
108
+ /**
109
+ * Delete an entity
110
+ */
111
+ async delete(entity) {
112
+ if (!this.metadata.primaryColumn) {
113
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
114
+ }
115
+ const primaryKey = this.metadata.primaryColumn.columnName;
116
+ const primaryValue = entity[this.metadata.primaryColumn.propertyKey];
117
+ if (primaryValue === undefined || primaryValue === null) {
118
+ throw new Error(`Cannot delete entity: primary key ${primaryKey} is ${primaryValue}`);
119
+ }
120
+ const sql = `DELETE FROM ${this.metadata.tableName} WHERE ${primaryKey} = $1`;
121
+ await this.connection.query(sql, [primaryValue]);
122
+ }
123
+ /**
124
+ * Delete by primary key
125
+ */
126
+ async deleteById(id) {
127
+ if (!this.metadata.primaryColumn) {
128
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
129
+ }
130
+ const primaryKey = this.metadata.primaryColumn.columnName;
131
+ const sql = `DELETE FROM ${this.metadata.tableName} WHERE ${primaryKey} = $1`;
132
+ await this.connection.query(sql, [id]);
133
+ }
134
+ /**
135
+ * Count all entities
136
+ */
137
+ async count() {
138
+ const sql = `SELECT COUNT(*) as count FROM ${this.metadata.tableName}`;
139
+ const result = await this.connection.query(sql);
140
+ const row = result.rows[0];
141
+ return parseInt(String(row.count), 10);
142
+ }
143
+ /**
144
+ * Check if entity exists by primary key
145
+ */
146
+ async exists(id) {
147
+ if (!this.metadata.primaryColumn) {
148
+ throw new Error(`Entity ${this.entityClass.name} has no primary key`);
149
+ }
150
+ const primaryKey = this.metadata.primaryColumn.columnName;
151
+ const sql = `SELECT 1 FROM ${this.metadata.tableName} WHERE ${primaryKey} = $1 LIMIT 1`;
152
+ const result = await this.connection.query(sql, [id]);
153
+ return result.rows.length > 0;
154
+ }
155
+ }
156
+ exports.Repository = Repository;
@@ -0,0 +1,9 @@
1
+ import type { EntityConstructor, EntityMetadata } from '../metadata/types';
2
+ /**
3
+ * Maps a database row to an entity instance
4
+ */
5
+ export declare function mapRowToEntity<T>(row: Record<string, unknown>, entityClass: EntityConstructor, metadata: EntityMetadata): T;
6
+ /**
7
+ * Maps multiple database rows to entity instances
8
+ */
9
+ export declare function mapRowsToEntities<T>(rows: Record<string, unknown>[], entityClass: EntityConstructor, metadata: EntityMetadata): T[];
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapRowToEntity = mapRowToEntity;
4
+ exports.mapRowsToEntities = mapRowsToEntities;
5
+ /**
6
+ * Maps a database row to an entity instance
7
+ */
8
+ function mapRowToEntity(row, entityClass, metadata) {
9
+ const entity = new entityClass();
10
+ // Map each column from the database row to the entity property
11
+ metadata.columns.forEach((col) => {
12
+ const value = row[col.columnName];
13
+ entity[col.propertyKey] = value;
14
+ });
15
+ return entity;
16
+ }
17
+ /**
18
+ * Maps multiple database rows to entity instances
19
+ */
20
+ function mapRowsToEntities(rows, entityClass, metadata) {
21
+ return rows.map((row) => mapRowToEntity(row, entityClass, metadata));
22
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "accio-orm",
3
+ "version": "0.1.0",
4
+ "description": "The summoning charm for Postgres - A lightweight TypeScript ORM",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "ts-node examples/basic-usage.ts",
16
+ "lint": "eslint src/",
17
+ "lint:fix": "eslint . --fix",
18
+ "type-check": "tsc --noEmit",
19
+ "test": "vitest",
20
+ "test:ui": "vitest --ui",
21
+ "test:run": "vitest run",
22
+ "coverage": "vitest run --coverage",
23
+ "prepublishOnly": "pnpm run build && pnpm run test:run"
24
+ },
25
+ "keywords": [
26
+ "orm",
27
+ "postgres",
28
+ "postgresql",
29
+ "typescript",
30
+ "database",
31
+ "sql",
32
+ "data-mapper"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/jidemusty/accio.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/jidemusty/accio/issues"
40
+ },
41
+ "homepage": "https://github.com/jidemusty/accio#readme",
42
+ "author": "Mustapha Adubiagbe",
43
+ "license": "MIT",
44
+ "packageManager": "pnpm@10.27.0",
45
+ "devDependencies": {
46
+ "@eslint/js": "^9.39.2",
47
+ "@types/node": "^25.0.3",
48
+ "@types/pg": "^8.16.0",
49
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
50
+ "@typescript-eslint/parser": "^8.51.0",
51
+ "@vitest/coverage-v8": "4.0.16",
52
+ "@vitest/ui": "^4.0.16",
53
+ "eslint": "^9.39.2",
54
+ "eslint-config-prettier": "^10.1.8",
55
+ "eslint-import-resolver-typescript": "^4.4.4",
56
+ "eslint-plugin-import": "^2.32.0",
57
+ "eslint-plugin-simple-import-sort": "^12.1.1",
58
+ "globals": "^17.0.0",
59
+ "pg": "^8.16.3",
60
+ "prettier": "^3.7.4",
61
+ "reflect-metadata": "^0.2.2",
62
+ "ts-node": "^10.9.2",
63
+ "tsx": "^4.21.0",
64
+ "typescript": "^5.9.3",
65
+ "typescript-eslint": "^8.52.0",
66
+ "vitest": "^4.0.16"
67
+ },
68
+ "peerDependencies": {
69
+ "pg": "^8.0.0",
70
+ "reflect-metadata": "^0.2.2"
71
+ }
72
+ }