supabase-test 0.0.1
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 +23 -0
- package/README.md +557 -0
- package/admin.d.ts +26 -0
- package/admin.js +182 -0
- package/connect.d.ts +19 -0
- package/connect.js +83 -0
- package/dist/README.md +557 -0
- package/dist/package.json +72 -0
- package/esm/admin.js +178 -0
- package/esm/connect.js +78 -0
- package/esm/index.js +6 -0
- package/esm/manager.js +136 -0
- package/esm/roles.js +32 -0
- package/esm/seed/adapters.js +23 -0
- package/esm/seed/csv.js +44 -0
- package/esm/seed/index.js +16 -0
- package/esm/seed/json.js +18 -0
- package/esm/seed/launchql.js +19 -0
- package/esm/seed/sqitch.js +17 -0
- package/esm/seed/types.js +1 -0
- package/esm/stream.js +43 -0
- package/esm/test-client.js +150 -0
- package/index.d.ts +6 -0
- package/index.js +22 -0
- package/manager.d.ts +25 -0
- package/manager.js +140 -0
- package/package.json +72 -0
- package/roles.d.ts +17 -0
- package/roles.js +38 -0
- package/seed/adapters.d.ts +4 -0
- package/seed/adapters.js +28 -0
- package/seed/csv.d.ts +9 -0
- package/seed/csv.js +49 -0
- package/seed/index.d.ts +16 -0
- package/seed/index.js +33 -0
- package/seed/json.d.ts +6 -0
- package/seed/json.js +21 -0
- package/seed/launchql.d.ts +2 -0
- package/seed/launchql.js +22 -0
- package/seed/sqitch.d.ts +2 -0
- package/seed/sqitch.js +20 -0
- package/seed/types.d.ts +13 -0
- package/seed/types.js +2 -0
- package/stream.d.ts +2 -0
- package/stream.js +46 -0
- package/test-client.d.ts +49 -0
- package/test-client.js +154 -0
package/admin.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdmin = void 0;
|
|
4
|
+
const logger_1 = require("@launchql/logger");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const pg_env_1 = require("pg-env");
|
|
8
|
+
const roles_1 = require("./roles");
|
|
9
|
+
const stream_1 = require("./stream");
|
|
10
|
+
const log = new logger_1.Logger('db-admin');
|
|
11
|
+
class DbAdmin {
|
|
12
|
+
config;
|
|
13
|
+
verbose;
|
|
14
|
+
roleConfig;
|
|
15
|
+
constructor(config, verbose = false, roleConfig) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.verbose = verbose;
|
|
18
|
+
this.roleConfig = roleConfig;
|
|
19
|
+
this.config = (0, pg_env_1.getPgEnvOptions)(config);
|
|
20
|
+
}
|
|
21
|
+
getEnv() {
|
|
22
|
+
return {
|
|
23
|
+
PGHOST: this.config.host,
|
|
24
|
+
PGPORT: String(this.config.port),
|
|
25
|
+
PGUSER: this.config.user,
|
|
26
|
+
PGPASSWORD: this.config.password
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
run(command) {
|
|
30
|
+
try {
|
|
31
|
+
(0, child_process_1.execSync)(command, {
|
|
32
|
+
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
...this.getEnv()
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (this.verbose)
|
|
39
|
+
log.success(`Executed: ${command}`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log.error(`Command failed: ${command}`);
|
|
43
|
+
if (this.verbose)
|
|
44
|
+
log.error(err.message);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
safeDropDb(name) {
|
|
49
|
+
try {
|
|
50
|
+
this.run(`dropdb "${name}"`);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (!err.message.includes('does not exist')) {
|
|
54
|
+
log.warn(`Could not drop database ${name}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
drop(dbName) {
|
|
59
|
+
this.safeDropDb(dbName ?? this.config.database);
|
|
60
|
+
}
|
|
61
|
+
dropTemplate(dbName) {
|
|
62
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}';"`);
|
|
63
|
+
this.drop(dbName);
|
|
64
|
+
}
|
|
65
|
+
create(dbName) {
|
|
66
|
+
const db = dbName ?? this.config.database;
|
|
67
|
+
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
|
|
68
|
+
}
|
|
69
|
+
createFromTemplate(template, dbName) {
|
|
70
|
+
const db = dbName ?? this.config.database;
|
|
71
|
+
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
|
|
72
|
+
}
|
|
73
|
+
installExtensions(extensions, dbName) {
|
|
74
|
+
const db = dbName ?? this.config.database;
|
|
75
|
+
const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
|
76
|
+
for (const extension of extList) {
|
|
77
|
+
this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
connectionString(dbName) {
|
|
81
|
+
const { user, password, host, port } = this.config;
|
|
82
|
+
const db = dbName ?? this.config.database;
|
|
83
|
+
return `postgres://${user}:${password}@${host}:${port}/${db}`;
|
|
84
|
+
}
|
|
85
|
+
createTemplateFromBase(base, template) {
|
|
86
|
+
this.run(`createdb -T "${base}" "${template}"`);
|
|
87
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
|
|
88
|
+
}
|
|
89
|
+
cleanupTemplate(template) {
|
|
90
|
+
try {
|
|
91
|
+
this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
|
|
95
|
+
}
|
|
96
|
+
this.safeDropDb(template);
|
|
97
|
+
}
|
|
98
|
+
async grantRole(role, user, dbName) {
|
|
99
|
+
const db = dbName ?? this.config.database;
|
|
100
|
+
const sql = `GRANT ${role} TO ${user};`;
|
|
101
|
+
await this.streamSql(sql, db);
|
|
102
|
+
}
|
|
103
|
+
async grantConnect(role, dbName) {
|
|
104
|
+
const db = dbName ?? this.config.database;
|
|
105
|
+
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
106
|
+
await this.streamSql(sql, db);
|
|
107
|
+
}
|
|
108
|
+
// TODO: make adminRole a configurable option
|
|
109
|
+
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
|
|
110
|
+
// DO NOT USE THIS FOR PRODUCTION
|
|
111
|
+
async createUserRole(user, password, dbName) {
|
|
112
|
+
const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
|
|
113
|
+
const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
|
|
114
|
+
const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
|
|
115
|
+
const sql = `
|
|
116
|
+
DO $$
|
|
117
|
+
BEGIN
|
|
118
|
+
-- Create role if it doesn't exist
|
|
119
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
|
|
120
|
+
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
|
|
121
|
+
END IF;
|
|
122
|
+
|
|
123
|
+
-- Grant anonymous role if not already granted
|
|
124
|
+
IF NOT EXISTS (
|
|
125
|
+
SELECT 1 FROM pg_auth_members am
|
|
126
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
127
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
128
|
+
WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
|
|
129
|
+
) THEN
|
|
130
|
+
GRANT ${anonRole} TO ${user};
|
|
131
|
+
END IF;
|
|
132
|
+
|
|
133
|
+
-- Grant authenticated role if not already granted
|
|
134
|
+
IF NOT EXISTS (
|
|
135
|
+
SELECT 1 FROM pg_auth_members am
|
|
136
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
137
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
138
|
+
WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
|
|
139
|
+
) THEN
|
|
140
|
+
GRANT ${authRole} TO ${user};
|
|
141
|
+
END IF;
|
|
142
|
+
|
|
143
|
+
-- Grant administrator role if not already granted
|
|
144
|
+
IF NOT EXISTS (
|
|
145
|
+
SELECT 1 FROM pg_auth_members am
|
|
146
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
147
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
148
|
+
WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
|
|
149
|
+
) THEN
|
|
150
|
+
GRANT ${adminRole} TO ${user};
|
|
151
|
+
END IF;
|
|
152
|
+
END $$;
|
|
153
|
+
`.trim();
|
|
154
|
+
await this.streamSql(sql, dbName);
|
|
155
|
+
}
|
|
156
|
+
loadSql(file, dbName) {
|
|
157
|
+
if (!(0, fs_1.existsSync)(file)) {
|
|
158
|
+
throw new Error(`Missing SQL file: ${file}`);
|
|
159
|
+
}
|
|
160
|
+
this.run(`psql -f ${file} ${dbName}`);
|
|
161
|
+
}
|
|
162
|
+
async streamSql(sql, dbName) {
|
|
163
|
+
await (0, stream_1.streamSql)({
|
|
164
|
+
...this.config,
|
|
165
|
+
database: dbName
|
|
166
|
+
}, sql);
|
|
167
|
+
}
|
|
168
|
+
async createSeededTemplate(templateName, adapter) {
|
|
169
|
+
const seedDb = this.config.database;
|
|
170
|
+
this.create(seedDb);
|
|
171
|
+
await adapter.seed({
|
|
172
|
+
admin: this,
|
|
173
|
+
config: this.config,
|
|
174
|
+
pg: null, // placeholder for PgTestClient
|
|
175
|
+
connect: null // placeholder for connection factory
|
|
176
|
+
});
|
|
177
|
+
this.cleanupTemplate(templateName);
|
|
178
|
+
this.createTemplateFromBase(seedDb, templateName);
|
|
179
|
+
this.drop(seedDb);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.DbAdmin = DbAdmin;
|
package/connect.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PgTestConnectionOptions } from '@launchql/types';
|
|
2
|
+
import { PgConfig } from 'pg-env';
|
|
3
|
+
import { DbAdmin } from './admin';
|
|
4
|
+
import { PgTestConnector } from './manager';
|
|
5
|
+
import { SeedAdapter } from './seed/types';
|
|
6
|
+
import { PgTestClient } from './test-client';
|
|
7
|
+
export declare const getPgRootAdmin: (connOpts?: PgTestConnectionOptions) => DbAdmin;
|
|
8
|
+
export interface GetConnectionOpts {
|
|
9
|
+
pg?: Partial<PgConfig>;
|
|
10
|
+
db?: Partial<PgTestConnectionOptions>;
|
|
11
|
+
}
|
|
12
|
+
export interface GetConnectionResult {
|
|
13
|
+
pg: PgTestClient;
|
|
14
|
+
db: PgTestClient;
|
|
15
|
+
admin: DbAdmin;
|
|
16
|
+
teardown: () => Promise<void>;
|
|
17
|
+
manager: PgTestConnector;
|
|
18
|
+
}
|
|
19
|
+
export declare const getConnections: (cn?: GetConnectionOpts, seedAdapters?: SeedAdapter[]) => Promise<GetConnectionResult>;
|
package/connect.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConnections = exports.getPgRootAdmin = void 0;
|
|
4
|
+
const env_1 = require("@launchql/env");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const pg_cache_1 = require("pg-cache");
|
|
7
|
+
const pg_env_1 = require("pg-env");
|
|
8
|
+
const admin_1 = require("./admin");
|
|
9
|
+
const manager_1 = require("./manager");
|
|
10
|
+
const roles_1 = require("./roles");
|
|
11
|
+
const seed_1 = require("./seed");
|
|
12
|
+
let manager;
|
|
13
|
+
const getPgRootAdmin = (connOpts = {}) => {
|
|
14
|
+
const opts = (0, pg_env_1.getPgEnvOptions)({
|
|
15
|
+
database: connOpts.rootDb
|
|
16
|
+
});
|
|
17
|
+
const admin = new admin_1.DbAdmin(opts, false, connOpts);
|
|
18
|
+
return admin;
|
|
19
|
+
};
|
|
20
|
+
exports.getPgRootAdmin = getPgRootAdmin;
|
|
21
|
+
const getConnOopts = (cn = {}) => {
|
|
22
|
+
const connect = (0, env_1.getConnEnvOptions)(cn.db);
|
|
23
|
+
const config = (0, pg_env_1.getPgEnvOptions)({
|
|
24
|
+
database: `${connect.prefix}${(0, crypto_1.randomUUID)()}`,
|
|
25
|
+
...cn.pg
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
pg: config,
|
|
29
|
+
db: connect
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.launchql()]) => {
|
|
33
|
+
cn = getConnOopts(cn);
|
|
34
|
+
const config = cn.pg;
|
|
35
|
+
const connOpts = cn.db;
|
|
36
|
+
const root = (0, exports.getPgRootAdmin)(connOpts);
|
|
37
|
+
await root.createUserRole(connOpts.connection.user, connOpts.connection.password, connOpts.rootDb);
|
|
38
|
+
const admin = new admin_1.DbAdmin(config, false, connOpts);
|
|
39
|
+
if (process.env.TEST_DB) {
|
|
40
|
+
config.database = process.env.TEST_DB;
|
|
41
|
+
}
|
|
42
|
+
else if (connOpts.template) {
|
|
43
|
+
admin.createFromTemplate(connOpts.template, config.database);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
admin.create(config.database);
|
|
47
|
+
admin.installExtensions(connOpts.extensions);
|
|
48
|
+
}
|
|
49
|
+
await admin.grantConnect(connOpts.connection.user, config.database);
|
|
50
|
+
manager = manager_1.PgTestConnector.getInstance();
|
|
51
|
+
const pg = manager.getClient(config);
|
|
52
|
+
const teardown = async () => {
|
|
53
|
+
manager.beginTeardown();
|
|
54
|
+
await (0, pg_cache_1.teardownPgPools)();
|
|
55
|
+
await manager.closeAll();
|
|
56
|
+
};
|
|
57
|
+
if (seedAdapters.length) {
|
|
58
|
+
try {
|
|
59
|
+
await seed_1.seed.compose(seedAdapters).seed({
|
|
60
|
+
connect: connOpts,
|
|
61
|
+
admin,
|
|
62
|
+
config: config,
|
|
63
|
+
pg: manager.getClient(config)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await teardown();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const dbConfig = {
|
|
72
|
+
...config,
|
|
73
|
+
user: connOpts.connection.user,
|
|
74
|
+
password: connOpts.connection.password
|
|
75
|
+
};
|
|
76
|
+
const db = manager.getClient(dbConfig, {
|
|
77
|
+
auth: connOpts.auth,
|
|
78
|
+
roles: connOpts.roles
|
|
79
|
+
});
|
|
80
|
+
db.setContext({ role: (0, roles_1.getDefaultRole)(connOpts) });
|
|
81
|
+
return { pg, db, teardown, manager, admin };
|
|
82
|
+
};
|
|
83
|
+
exports.getConnections = getConnections;
|