Skip to content
This repository was archived by the owner on May 8, 2025. It is now read-only.

Commit 99994a3

Browse files
author
Valentin Vasilyev
committed
Initial commit
0 parents  commit 99994a3

File tree

9 files changed

+3899
-0
lines changed

9 files changed

+3899
-0
lines changed

.eslintrc

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
"env": {
3+
"commonjs": true,
4+
"es6": true,
5+
"node": true,
6+
"jest": true,
7+
},
8+
"extends": "eslint:recommended",
9+
"rules": {
10+
// ...
11+
},
12+
};

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
yarn-error.log
3+
.idea

.prettierrc

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"printWidth": 120,
3+
"tabWidth": 2,
4+
"useTabs": false,
5+
"semi": false,
6+
"singleQuote": true,
7+
"jsxSingleQuote": true,
8+
"trailingComma": "es5",
9+
"bracketSpacing": true,
10+
"jsxBracketSameLine": false,
11+
"arrowParens": "always"
12+
}

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
WEERM
2+
============================
3+
4+
Super tiny ORM for PG + Node
5+

jest.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
testEnvironment: "node",
3+
};

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@fingerprintjs/weerm",
3+
"version": "1.0.0",
4+
"description": "Really tiny ORM for PostgreSQL",
5+
"main": "index.js",
6+
"repository": "https://github.com/fingerprintjs/weerm",
7+
"author": "FingerprintJS",
8+
"license": "MIT",
9+
"private": false,
10+
"dependencies": {
11+
"pg": "^8.2.1"
12+
},
13+
"devDependencies": {
14+
"jest": "^26.1.0",
15+
"prettier": "^2.0.5"
16+
},
17+
"scripts": {
18+
"test": "NODE_ENV=test jest"
19+
}
20+
}

src/index.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
const { Pool } = require('pg')
2+
const UNIQUE_INDEX_VIOLATION_ERR_CODE = '23505'
3+
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
4+
pool.on('acquire', () => {})
5+
pool.on('remove', () => {})
6+
pool.on('connect', () => {})
7+
pool.on('error', (e) => {})
8+
9+
const shutdownPool = async () => {
10+
await pool.end()
11+
}
12+
13+
const findOne = async (tableName, condition) => {
14+
let rows = await find(tableName, condition)
15+
if (rows && rows.length > 0) {
16+
return rows[0]
17+
}
18+
return null
19+
}
20+
21+
const find = async (tableName, condition) => {
22+
let parts = [`SELECT * FROM ${tableName}`]
23+
let conditionQuery = buildConditionSql(condition)
24+
parts.push(conditionQuery)
25+
let query = parts.join(' ')
26+
let dollarValues = Object.keys(condition).map((key) => condition[key])
27+
let result = await pool.query(query, dollarValues)
28+
return result.rows
29+
}
30+
31+
const insert = async (tr, tableName, columnValues, returning = ['id']) => {
32+
let parts = [`INSERT INTO ${tableName}`]
33+
let insertColumns = Object.keys(columnValues)
34+
parts.push(`(${insertColumns.join(', ')})`)
35+
parts.push('VALUES')
36+
let dollars = Object.keys(columnValues).map((_, i) => `$${i + 1}`)
37+
parts.push(`(${dollars.join(', ')})`)
38+
parts.push(`RETURNING ${returning.join(', ')}`)
39+
let dollarValues = Object.keys(columnValues).map((key) => columnValues[key])
40+
let query = parts.join(' ')
41+
try {
42+
let result = await (tr || pool).query(query, dollarValues)
43+
if (result.rows && result.rows.length > 0) {
44+
return result.rows[0]
45+
}
46+
} catch (e) {
47+
if (e.code && e.code === UNIQUE_INDEX_VIOLATION_ERR_CODE) {
48+
throwUniqueIndexViolationError(e)
49+
}
50+
throw e
51+
}
52+
}
53+
54+
const update = async (tr, tableName, columnValues, condition) => {
55+
let parts = [`UPDATE ${tableName} SET`]
56+
let updateParts = []
57+
Object.keys(columnValues).forEach((columnName, i) => {
58+
updateParts.push(`${columnName} = $${i + 1}`)
59+
})
60+
parts.push(updateParts.join(', '))
61+
parts.push(buildConditionSql(condition, updateParts.length))
62+
let query = parts.join(' ')
63+
let dollarValues = Object.keys(columnValues).map((key) => columnValues[key])
64+
if (condition !== {}) {
65+
dollarValues = dollarValues.concat(Object.keys(condition).map((key) => condition[key]))
66+
}
67+
try {
68+
await (tr || pool).query(query, dollarValues)
69+
} catch (e) {
70+
if (e.code && e.code === UNIQUE_INDEX_VIOLATION_ERR_CODE) {
71+
throwUniqueIndexViolationError(e)
72+
}
73+
throw e
74+
}
75+
return true
76+
}
77+
78+
const del = async (tr, tableName, condition) => {
79+
let parts = [`DELETE FROM ${tableName}`]
80+
parts.push(buildConditionSql(condition))
81+
let query = parts.join(' ')
82+
let dollarValues = Object.keys(condition).map((key) => condition[key])
83+
let result = await (tr || pool).query(query, dollarValues)
84+
return result.rows
85+
}
86+
87+
const query = async (tr, sql, dollarValues) => {
88+
let result = await (tr || pool).query(sql, dollarValues)
89+
return result
90+
}
91+
92+
const buildConditionSql = (condition, startDollarNumbering = 0) => {
93+
if (!condition || Object.keys(condition).length === 0) {
94+
return null
95+
}
96+
let parts = []
97+
let conditionParts = []
98+
Object.keys(condition).forEach((key, i) => {
99+
let value = condition[key]
100+
// https://bit.ly/2yNyzoe
101+
if (Array.isArray(value)) {
102+
conditionParts.push(`${key} = ANY($${startDollarNumbering + i + 1})`)
103+
} else {
104+
conditionParts.push(`${key} = $${startDollarNumbering + i + 1}`)
105+
}
106+
})
107+
if (conditionParts.length > 0) {
108+
parts.push('WHERE')
109+
parts.push(conditionParts.join(' AND '))
110+
}
111+
return parts.join(' ')
112+
}
113+
114+
const withTransaction = async (cb) => {
115+
const client = await pool.connect()
116+
try {
117+
await client.query('BEGIN')
118+
await cb(client)
119+
await client.query('COMMIT')
120+
} catch (e) {
121+
await client.query('ROLLBACK')
122+
throw e
123+
} finally {
124+
client.release()
125+
}
126+
}
127+
128+
const mapToColumns = (attrs, map) => {
129+
if (!attrs) {
130+
return {}
131+
}
132+
return Object.keys(attrs).reduce((obj, key) => {
133+
const column = map[key]
134+
if (!column) {
135+
throw new Error(`Unknown attribute: ${key}`)
136+
}
137+
obj[column] = attrs[key]
138+
return obj
139+
}, {})
140+
}
141+
142+
const mapFromColumns = (row, map) => {
143+
if (!row) {
144+
return null
145+
}
146+
return Object.keys(map).reduce((obj, key) => {
147+
const column = map[key]
148+
obj[key] = row[column]
149+
return obj
150+
}, {})
151+
}
152+
153+
class UniqueIndexError extends Error {
154+
constructor({ table, constraint, detail }) {
155+
super(detail)
156+
this.table = table
157+
this.constraint = constraint
158+
this.columns = this.parseColumns(detail)
159+
}
160+
161+
parseColumns(detail) {
162+
// error message usually looks like this (in case of a single-column) index violation
163+
// Key (email)=([email protected]) already exists.
164+
// or like this (in case of a multi-column) index violation
165+
// Key (customer_id, display_name)=(cus_JOgWoKqN6pizGN, sub1) already exists.
166+
let parts = detail.split('=')
167+
parts = parts[0].split(/[\(\)]/)
168+
let rawColumns = parts[1]
169+
return rawColumns.split(', ')
170+
}
171+
}
172+
173+
function throwUniqueIndexViolationError(e) {
174+
throw new UniqueIndexError(e)
175+
}
176+
177+
module.exports = {
178+
shutdownPool,
179+
find,
180+
findOne,
181+
insert,
182+
update,
183+
del,
184+
mapToColumns,
185+
mapFromColumns,
186+
withTransaction,
187+
query,
188+
UniqueIndexError,
189+
}

tests/index.test.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const {UniqueIndexError} = require("..//src/index");
2+
3+
test("it parses single column constraint", () => {
4+
let attrs = {
5+
table: "test_table",
6+
constraint: "test_constraint",
7+
detail: "Key (email)=([email protected]) already exists."
8+
};
9+
let err = new UniqueIndexError(attrs)
10+
expect(err.columns).toEqual(["email"]);
11+
});
12+
13+
test("it parses multiple column constraint", () => {
14+
let attrs = {
15+
table: "test_table",
16+
constraint: "test_constraint",
17+
detail: "Key (customer_id, display_name)=(customer1234, Corporation1) already exists."
18+
};
19+
let err = new UniqueIndexError(attrs)
20+
expect(err.columns).toEqual(["customer_id", "display_name"]);
21+
});

0 commit comments

Comments
 (0)