@quillsql/node 0.2.7 → 0.2.9
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 +55 -0
- package/dist/clients/QuillServerClient.js +28 -0
- package/dist/db/CachedPools.js +91 -0
- package/dist/index.ispec.js +41 -0
- package/dist/index.js +103 -0
- package/dist/index.uspec.js +38 -0
- package/dist/models/Cache.js +2 -0
- package/dist/models/Database.js +2 -0
- package/dist/models/Formats.js +2 -0
- package/dist/models/Quill.js +2 -0
- package/dist/utils/Error.js +18 -0
- package/dist/utils/RunQueryProcesses.js +48 -0
- package/examples/node-server/app.ts +35 -0
- package/jest.config.js +19 -0
- package/package.json +26 -15
- package/src/clients/QuillServerClient.ts +23 -0
- package/src/db/CachedPools.ts +97 -0
- package/src/index.ispec.ts +36 -0
- package/src/index.ts +144 -0
- package/src/index.uspec.ts +38 -0
- package/src/models/Cache.ts +18 -0
- package/src/models/Database.ts +5 -0
- package/src/models/Formats.ts +19 -0
- package/src/models/Quill.ts +68 -0
- package/src/utils/Error.ts +21 -0
- package/src/utils/RunQueryProcesses.ts +58 -0
- package/tsconfig.json +11 -106
- package/.gitattributes +0 -2
- package/LICENSE +0 -21
- package/index.js +0 -171
- package/index.ts +0 -272
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Quill Node SDK
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
$ npm install @quillsql/node
|
|
7
|
+
|
|
8
|
+
# Or, if you prefer yarn
|
|
9
|
+
$ yarn add @quillsql/node
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Instantiate quill with your credentials and add the below POST endpoint.
|
|
15
|
+
|
|
16
|
+
Note that we assume you have an organization id on the user returned by your auth middleware. Queries will not work properly without the organization id.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const quill = require("@quillsql/node")({
|
|
20
|
+
privateKey: process.env.QULL_PRIVATE_KEY,
|
|
21
|
+
databaseConnectionString: process.env.POSTGRES_READ,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// "authenticateJWT" is your own pre-existing auth middleware
|
|
25
|
+
app.post("/quill", authenticateJWT, async (req, res) => {
|
|
26
|
+
// assuming user fetched via auth middleware has an organizationId
|
|
27
|
+
const { organizationId } = req.user;
|
|
28
|
+
const { metadata } = req.body;
|
|
29
|
+
const result = await quill.query({
|
|
30
|
+
orgId: organizationId,
|
|
31
|
+
metadata,
|
|
32
|
+
});
|
|
33
|
+
res.send(result);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## For local testing (dev purposes only)
|
|
38
|
+
|
|
39
|
+
Create an .env file with the following key-value pairs:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
QUILL_PRIVATE_KEY=
|
|
43
|
+
DB_URL=
|
|
44
|
+
ENV=development
|
|
45
|
+
BACKEND_URL=
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use the following commands to start a locally hosted dev server.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
npm install
|
|
52
|
+
npm run dev-server
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
You should be able to ping your local server at `http://localhost:3000`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
|
+
/** This client is currently not used but is a good design pratice */
|
|
17
|
+
class QuillServerClient {
|
|
18
|
+
constructor(privateKey) {
|
|
19
|
+
this.baseUrl = "";
|
|
20
|
+
this.config = { headers: { Authorization: `Bearer ${privateKey}` } };
|
|
21
|
+
}
|
|
22
|
+
postQuill(route, payload) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
return axios_1.default.post(`${this.baseUrl}/${route}`, payload, this.config);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.default = QuillServerClient;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CachedPool = void 0;
|
|
13
|
+
const pg_1 = require("pg");
|
|
14
|
+
const redis_1 = require("redis");
|
|
15
|
+
const Error_1 = require("../utils/Error");
|
|
16
|
+
class PgError extends Error {
|
|
17
|
+
// Add other properties if needed
|
|
18
|
+
constructor(detail, hint, position) {
|
|
19
|
+
super();
|
|
20
|
+
this.detail = detail;
|
|
21
|
+
this.hint = hint;
|
|
22
|
+
this.position = position;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The TTL for new cache entries (default: 1h) */
|
|
26
|
+
const DEFAULT_CACHE_TTL = 24 * 60 * 60;
|
|
27
|
+
class CachedPool {
|
|
28
|
+
constructor(config, cacheConfig = {}) {
|
|
29
|
+
var _a;
|
|
30
|
+
this.pool = new pg_1.Pool(config);
|
|
31
|
+
this.ttl = (_a = cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.ttl) !== null && _a !== void 0 ? _a : DEFAULT_CACHE_TTL;
|
|
32
|
+
this.cache = this.getCache(cacheConfig);
|
|
33
|
+
}
|
|
34
|
+
query(text, values) {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
try {
|
|
37
|
+
if (!this.cache) {
|
|
38
|
+
const results = yield this.pool.query(text, values);
|
|
39
|
+
return {
|
|
40
|
+
fields: results.fields.map((field) => ({
|
|
41
|
+
name: field.name,
|
|
42
|
+
dataTypeID: field.dataTypeID,
|
|
43
|
+
})),
|
|
44
|
+
rows: results.rows,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const key = `${this.orgId}:${text}`;
|
|
48
|
+
const cachedResult = yield this.cache.get(key);
|
|
49
|
+
if (cachedResult) {
|
|
50
|
+
return JSON.parse(cachedResult);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const newResult = yield this.pool.query(text, values);
|
|
54
|
+
const newResultString = JSON.stringify(newResult);
|
|
55
|
+
yield this.cache.set(key, newResultString, "EX", DEFAULT_CACHE_TTL);
|
|
56
|
+
return {
|
|
57
|
+
fields: newResult.fields.map((field) => ({
|
|
58
|
+
name: field.name,
|
|
59
|
+
dataTypeID: field.dataTypeID,
|
|
60
|
+
})),
|
|
61
|
+
rows: newResult.rows,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if ((0, Error_1.isSuperset)(err, PgError)) {
|
|
67
|
+
throw new PgError(err.detail, err.hint, err.position);
|
|
68
|
+
}
|
|
69
|
+
else if (err instanceof Error) {
|
|
70
|
+
throw new Error(err.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Configures and returns a cache instance or null if none could be created.
|
|
77
|
+
*/
|
|
78
|
+
getCache({ username, password, host, port, cacheType, }) {
|
|
79
|
+
if (cacheType === "redis" || cacheType === "rediss") {
|
|
80
|
+
const redisURL = `${cacheType}://${username}:${password}@${host}:${port}`;
|
|
81
|
+
return (0, redis_1.createClient)({ url: redisURL });
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
close() {
|
|
86
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
yield this.pool.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.CachedPool = CachedPool;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const _1 = __importDefault(require("."));
|
|
16
|
+
require("dotenv/config");
|
|
17
|
+
const HOST = process.env.ENV === "development"
|
|
18
|
+
? "http://localhost:8080"
|
|
19
|
+
: "https://quill-344421.uc.r.appspot.com";
|
|
20
|
+
describe("Quill", () => {
|
|
21
|
+
let quill;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
quill = new _1.default(process.env.QUILL_PRIVATE_KEY, process.env.DB_URL);
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe("query", () => {
|
|
29
|
+
it("org - should return ", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
const metadata = {
|
|
31
|
+
task: "orgs",
|
|
32
|
+
clientId: "62cda15d7c9fcca7bc0a3689",
|
|
33
|
+
};
|
|
34
|
+
const result = yield quill.query({
|
|
35
|
+
orgId: "2",
|
|
36
|
+
metadata,
|
|
37
|
+
});
|
|
38
|
+
// TODO - add assertions
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const CachedPools_1 = require("./db/CachedPools");
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
require("dotenv/config");
|
|
18
|
+
const RunQueryProcesses_1 = require("./utils/RunQueryProcesses");
|
|
19
|
+
const HOST = process.env.ENV === "development"
|
|
20
|
+
? "http://localhost:8080"
|
|
21
|
+
: "https://quill-344421.uc.r.appspot.com";
|
|
22
|
+
/**
|
|
23
|
+
* Quill - Fullstack API Platform for Dashboards and Reporting.
|
|
24
|
+
*/
|
|
25
|
+
class Quill {
|
|
26
|
+
constructor(privateKey, databaseConnectionString, cache = {}) {
|
|
27
|
+
this.ssl = { rejectUnauthorized: false };
|
|
28
|
+
this.baseUrl = HOST;
|
|
29
|
+
this.config = { headers: { Authorization: `Bearer ${privateKey}` } };
|
|
30
|
+
this.connectionString = databaseConnectionString;
|
|
31
|
+
this.ssl = { rejectUnauthorized: false };
|
|
32
|
+
this.targetPool = new CachedPools_1.CachedPool({ connectionString: this.connectionString, ssl: this.ssl }, cache);
|
|
33
|
+
}
|
|
34
|
+
query({ orgId, metadata }) {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
this.targetPool.orgId = orgId;
|
|
37
|
+
try {
|
|
38
|
+
// Initial Query Request
|
|
39
|
+
const preQueryResults = yield this.runQueries(metadata.preQueries);
|
|
40
|
+
const response = yield this.postQuill(metadata.task, Object.assign(Object.assign({}, metadata), { orgId,
|
|
41
|
+
preQueryResults }));
|
|
42
|
+
const results = yield this.runQueries(response.queries, response.runQueryConfig);
|
|
43
|
+
// if there is no metadata object in the response, create one
|
|
44
|
+
if (!response.metadata) {
|
|
45
|
+
response.metadata = {};
|
|
46
|
+
}
|
|
47
|
+
// if there is a single query array in queryResults and the rows and field objects to metadata
|
|
48
|
+
if ((results === null || results === void 0 ? void 0 : results.queryResults.length) === 1) {
|
|
49
|
+
const queryResults = results.queryResults[0];
|
|
50
|
+
if (queryResults.rows) {
|
|
51
|
+
response.metadata.rows = queryResults.rows;
|
|
52
|
+
}
|
|
53
|
+
if (queryResults.fields) {
|
|
54
|
+
response.metadata.fields = queryResults.fields;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
data: response.metadata || null,
|
|
59
|
+
queries: results,
|
|
60
|
+
status: "success",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return { status: "error", error: err.message };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
runQueries(queries, runQueryConfig) {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
let results;
|
|
71
|
+
if (!queries)
|
|
72
|
+
return Object.assign(Object.assign({}, results), { queryResults: [] });
|
|
73
|
+
if (runQueryConfig === null || runQueryConfig === void 0 ? void 0 : runQueryConfig.arrayToMap) {
|
|
74
|
+
return Object.assign(Object.assign({}, results), { mappedQueries: yield (0, RunQueryProcesses_1.mapQueries)(queries, runQueryConfig.arrayToMap, this.targetPool) });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const queryResults = yield Promise.all(queries.map((query) => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
return yield this.targetPool.query(query);
|
|
79
|
+
})));
|
|
80
|
+
results = Object.assign(Object.assign({}, results), { queryResults });
|
|
81
|
+
if (runQueryConfig === null || runQueryConfig === void 0 ? void 0 : runQueryConfig.getSchema) {
|
|
82
|
+
results = Object.assign(Object.assign({}, results), { columns: yield (0, RunQueryProcesses_1.getTableSchema)(queryResults[0], this.targetPool) });
|
|
83
|
+
}
|
|
84
|
+
if (runQueryConfig === null || runQueryConfig === void 0 ? void 0 : runQueryConfig.removeFields) {
|
|
85
|
+
results = Object.assign(Object.assign({}, results), { queryResults: (0, RunQueryProcesses_1.removeFields)(queryResults, runQueryConfig.removeFields) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
postQuill(path, payload) {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
const response = yield axios_1.default.post(`${this.baseUrl}/sdk/${path}`, payload, this.config);
|
|
94
|
+
return response.data;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
close() {
|
|
98
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
yield this.targetPool.close();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.default = Quill;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const _1 = __importDefault(require("."));
|
|
7
|
+
jest.mock(".");
|
|
8
|
+
describe("Quill", () => {
|
|
9
|
+
let quill;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
quill = new _1.default("dummy_private_key", "dummy_db_url");
|
|
12
|
+
quill.targetPool.query = jest.fn().mockResolvedValue([]);
|
|
13
|
+
});
|
|
14
|
+
describe("query", () => {
|
|
15
|
+
it("return nothing when suppling no queries", () => {
|
|
16
|
+
const metadata = {
|
|
17
|
+
task: "test",
|
|
18
|
+
queries: [],
|
|
19
|
+
};
|
|
20
|
+
const result = quill.query({
|
|
21
|
+
orgId: "dummy",
|
|
22
|
+
metadata,
|
|
23
|
+
});
|
|
24
|
+
expect(result).resolves.toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
it("returns an error for the improper query", () => {
|
|
27
|
+
const metadata = {
|
|
28
|
+
task: "test",
|
|
29
|
+
queries: ["SELECT * FROM test"],
|
|
30
|
+
};
|
|
31
|
+
const result = quill.query({
|
|
32
|
+
orgId: "dummy",
|
|
33
|
+
metadata,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
// Add more test cases as needed
|
|
38
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSuperset = exports.PgError = void 0;
|
|
4
|
+
class PgError extends Error {
|
|
5
|
+
}
|
|
6
|
+
exports.PgError = PgError;
|
|
7
|
+
function isSuperset(obj, baseClass) {
|
|
8
|
+
// Get the property names of the base class
|
|
9
|
+
const baseProps = Object.getOwnPropertyNames(baseClass.prototype);
|
|
10
|
+
// Check if the object has all the properties of the base class
|
|
11
|
+
for (const prop of baseProps) {
|
|
12
|
+
if (!obj.hasOwnProperty(prop)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
exports.isSuperset = isSuperset;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.mapQueries = exports.removeFields = exports.getTableSchema = void 0;
|
|
13
|
+
function getTableSchema(queryResults, targetPool) {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
const typesQuery = yield targetPool.query("select typname, oid, typarray from pg_type order by oid;");
|
|
16
|
+
const schema = queryResults[0].fields.map((field) => {
|
|
17
|
+
return {
|
|
18
|
+
fieldType: typesQuery.rows.filter((type) => field.dataTypeID === type.oid)[0].typname,
|
|
19
|
+
name: field.name,
|
|
20
|
+
displayName: field.name,
|
|
21
|
+
isVisible: true,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return schema;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.getTableSchema = getTableSchema;
|
|
28
|
+
function removeFields(queryResults, fieldsToRemove) {
|
|
29
|
+
const fields = queryResults.fields.filter((field) => fieldsToRemove.includes(field.name));
|
|
30
|
+
const rows = queryResults.map((row) => {
|
|
31
|
+
fieldsToRemove.forEach((field) => {
|
|
32
|
+
delete row[field];
|
|
33
|
+
});
|
|
34
|
+
return { fields, rows };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
exports.removeFields = removeFields;
|
|
38
|
+
function mapQueries(queries, arrayToMap, targetPool) {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
let arrayToMapResult = [];
|
|
41
|
+
for (let i = 0; i < queries.length; i++) {
|
|
42
|
+
const queryResult = yield targetPool.query(queries[i]);
|
|
43
|
+
arrayToMapResult.push(Object.assign(Object.assign({}, arrayToMap.arr[i]), { [arrayToMap.field]: queryResult.rows }));
|
|
44
|
+
}
|
|
45
|
+
return arrayToMapResult;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
exports.mapQueries = mapQueries;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Write a simple node server to test the SDK
|
|
2
|
+
|
|
3
|
+
import express from "express";
|
|
4
|
+
import Quill from "../../src/index";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const port = 3000;
|
|
9
|
+
|
|
10
|
+
const quill = new Quill(
|
|
11
|
+
process.env.QUILL_PRIVATE_KEY as string,
|
|
12
|
+
process.env.DB_URL as string,
|
|
13
|
+
{}
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
app.use(cors());
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
|
|
19
|
+
app.get("/", (req, res) => {
|
|
20
|
+
res.send("Hello World!");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
app.post("/quill", async (req, res) => {
|
|
24
|
+
const organizationId = req.body.orgId;
|
|
25
|
+
const { metadata } = req.body;
|
|
26
|
+
const result = await quill.query({
|
|
27
|
+
orgId: organizationId,
|
|
28
|
+
metadata,
|
|
29
|
+
});
|
|
30
|
+
res.send(result);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.listen(port, () => {
|
|
34
|
+
console.log(`Example app listening at http://localhost:${port}`);
|
|
35
|
+
});
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"browser": false,
|
|
3
|
+
"automock": false,
|
|
4
|
+
"testEnvironment": "node",
|
|
5
|
+
"collectCoverage": false,
|
|
6
|
+
"transform": {
|
|
7
|
+
".(ts|tsx)": "ts-jest"
|
|
8
|
+
},
|
|
9
|
+
"transformIgnorePatterns": ['^.+\\.(js|json)$'],
|
|
10
|
+
"testRegex": "(/__tests__/.*|src.*?\\.(ispec|uspec))\\.(ts)$",
|
|
11
|
+
"testPathIgnorePatterns": [
|
|
12
|
+
"//node_modules/"
|
|
13
|
+
],
|
|
14
|
+
"moduleFileExtensions": [
|
|
15
|
+
"ts",
|
|
16
|
+
"js",
|
|
17
|
+
"json"
|
|
18
|
+
]
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillsql/node",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Quill SDK for
|
|
5
|
-
"main": "dist/index.
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"description": "Quill's client SDK for node backends.",
|
|
5
|
+
"main": "dist/index.ts",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "tsc",
|
|
8
|
-
"
|
|
7
|
+
"build": "tsc --outDir dist",
|
|
8
|
+
"start:dev": "nodemon ./examples/node-server/app.ts",
|
|
9
|
+
"integration-tests": "jest '.*\\.ispec\\.ts$'",
|
|
10
|
+
"test": "tsc src && jest --detectOpenHandles"
|
|
11
|
+
},
|
|
12
|
+
"author": "lagambino",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/cors": "^2.8.17",
|
|
16
|
+
"@types/express": "^4.17.21",
|
|
17
|
+
"@types/jest": "^29.5.11",
|
|
18
|
+
"@types/node": "^20.10.7",
|
|
19
|
+
"@types/pg": "^8.10.9",
|
|
20
|
+
"cors": "^2.8.5",
|
|
21
|
+
"express": "^4.18.2",
|
|
22
|
+
"jest": "^29.7.0",
|
|
23
|
+
"nodemon": "^3.0.2",
|
|
24
|
+
"ts-node": "^10.9.2",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
9
26
|
},
|
|
10
|
-
"author": "Quill",
|
|
11
|
-
"license": "MIT",
|
|
12
27
|
"dependencies": {
|
|
13
|
-
"axios": "^
|
|
28
|
+
"axios": "^1.6.5",
|
|
14
29
|
"dotenv": "^16.3.1",
|
|
15
|
-
"pg": "^8.11.
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@types/node": "^20.4.2",
|
|
20
|
-
"ts-node": "^10.9.1",
|
|
21
|
-
"typescript": "^5.1.6"
|
|
30
|
+
"pg": "^8.11.3",
|
|
31
|
+
"redis": "^4.6.12",
|
|
32
|
+
"ts-jest": "^29.1.1"
|
|
22
33
|
}
|
|
23
34
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
/** This client is currently not used but is a good design pratice */
|
|
4
|
+
|
|
5
|
+
class QuillServerClient {
|
|
6
|
+
private baseUrl: string;
|
|
7
|
+
private config: {
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
constructor(privateKey: string) {
|
|
14
|
+
this.baseUrl = "";
|
|
15
|
+
this.config = { headers: { Authorization: `Bearer ${privateKey}` } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async postQuill(route: string, payload: any): Promise<any> {
|
|
19
|
+
return axios.post(`${this.baseUrl}/${route}`, payload, this.config);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default QuillServerClient;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import { Mapable, CacheCredentials } from "../models/Cache";
|
|
3
|
+
import { QuillConfig } from "../models/Quill";
|
|
4
|
+
import { createClient } from "redis";
|
|
5
|
+
import { isSuperset } from "../utils/Error";
|
|
6
|
+
|
|
7
|
+
class PgError extends Error {
|
|
8
|
+
code?: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
position?: string;
|
|
12
|
+
// Add other properties if needed
|
|
13
|
+
constructor(detail?: string, hint?: string, position?: string) {
|
|
14
|
+
super();
|
|
15
|
+
this.detail = detail;
|
|
16
|
+
this.hint = hint;
|
|
17
|
+
this.position = position;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The TTL for new cache entries (default: 1h) */
|
|
22
|
+
const DEFAULT_CACHE_TTL = 24 * 60 * 60;
|
|
23
|
+
|
|
24
|
+
export class CachedPool {
|
|
25
|
+
public pool: Pool;
|
|
26
|
+
public orgId: any;
|
|
27
|
+
public ttl: number;
|
|
28
|
+
public cache: Mapable | null;
|
|
29
|
+
|
|
30
|
+
constructor(config: any, cacheConfig: Partial<CacheCredentials> = {}) {
|
|
31
|
+
this.pool = new Pool(config);
|
|
32
|
+
this.ttl = cacheConfig?.ttl ?? DEFAULT_CACHE_TTL;
|
|
33
|
+
this.cache = this.getCache(cacheConfig);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async query(text: string, values?: any[]): Promise<any> {
|
|
37
|
+
try {
|
|
38
|
+
if (!this.cache) {
|
|
39
|
+
const results = await this.pool.query(text, values);
|
|
40
|
+
return {
|
|
41
|
+
fields: results.fields.map((field: any) => ({
|
|
42
|
+
name: field.name,
|
|
43
|
+
dataTypeID: field.dataTypeID,
|
|
44
|
+
})),
|
|
45
|
+
rows: results.rows,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const key: string = `${this.orgId}:${text}`;
|
|
49
|
+
const cachedResult: string | null = await this.cache.get(key);
|
|
50
|
+
if (cachedResult) {
|
|
51
|
+
return JSON.parse(cachedResult);
|
|
52
|
+
} else {
|
|
53
|
+
const newResult: any = await this.pool.query(text, values);
|
|
54
|
+
const newResultString: string = JSON.stringify(newResult);
|
|
55
|
+
await this.cache.set(key, newResultString, "EX", DEFAULT_CACHE_TTL);
|
|
56
|
+
return {
|
|
57
|
+
fields: newResult.fields.map((field: any) => ({
|
|
58
|
+
name: field.name,
|
|
59
|
+
dataTypeID: field.dataTypeID,
|
|
60
|
+
})),
|
|
61
|
+
rows: newResult.rows,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (isSuperset(err, PgError)) {
|
|
66
|
+
throw new PgError(
|
|
67
|
+
(err as any).detail,
|
|
68
|
+
(err as any).hint,
|
|
69
|
+
(err as any).position
|
|
70
|
+
);
|
|
71
|
+
} else if (err instanceof Error) {
|
|
72
|
+
throw new Error(err.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Configures and returns a cache instance or null if none could be created.
|
|
79
|
+
*/
|
|
80
|
+
private getCache({
|
|
81
|
+
username,
|
|
82
|
+
password,
|
|
83
|
+
host,
|
|
84
|
+
port,
|
|
85
|
+
cacheType,
|
|
86
|
+
}: QuillConfig["cache"]): Mapable | null {
|
|
87
|
+
if (cacheType === "redis" || cacheType === "rediss") {
|
|
88
|
+
const redisURL = `${cacheType}://${username}:${password}@${host}:${port}`;
|
|
89
|
+
return createClient({ url: redisURL }) as Mapable;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async close() {
|
|
95
|
+
await this.pool.end();
|
|
96
|
+
}
|
|
97
|
+
}
|