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.
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/dist/connection/Connection.d.ts +29 -0
- package/dist/connection/Connection.js +63 -0
- package/dist/connection/types.d.ts +7 -0
- package/dist/connection/types.js +2 -0
- package/dist/decorators/Column.d.ts +23 -0
- package/dist/decorators/Column.js +34 -0
- package/dist/decorators/PrimaryColumn.d.ts +9 -0
- package/dist/decorators/PrimaryColumn.js +26 -0
- package/dist/decorators/Table.d.ts +10 -0
- package/dist/decorators/Table.js +21 -0
- package/dist/examples/test-connection.d.ts +1 -0
- package/dist/examples/test-connection.js +36 -0
- package/dist/examples/test-decorators.d.ts +1 -0
- package/dist/examples/test-decorators.js +40 -0
- package/dist/examples/test-metadata.d.ts +1 -0
- package/dist/examples/test-metadata.js +110 -0
- package/dist/examples/test-querybuilder.d.ts +1 -0
- package/dist/examples/test-querybuilder.js +142 -0
- package/dist/examples/test-repository.d.ts +1 -0
- package/dist/examples/test-repository.js +111 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +23 -0
- package/dist/metadata/MetadataStorage.d.ts +30 -0
- package/dist/metadata/MetadataStorage.js +82 -0
- package/dist/metadata/types.d.ts +7 -0
- package/dist/metadata/types.js +2 -0
- package/dist/query/QueryBuilder.d.ts +64 -0
- package/dist/query/QueryBuilder.js +180 -0
- package/dist/repository/Repository.d.ts +43 -0
- package/dist/repository/Repository.js +156 -0
- package/dist/utils/entityMapper.d.ts +9 -0
- package/dist/utils/entityMapper.js +22 -0
- 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
|
+
}
|