Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More ā†’
Socket
Sign inDemoInstall
Socket

postgrate

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

postgrate - npm Package Compare versions

Comparing version 1.0.1 to 1.1.0

dist/bin/config.js

30

dist/bin/commands/init.command.js
import fs from 'fs';
export default function () {
if (!fs.existsSync('db')) {
fs.mkdirSync('db');
import config from '../config.js';
export default function (createConfigFile = false) {
if (createConfigFile) {
fs.writeFileSync('.postgraterc', JSON.stringify({
rootDirectory: 'db',
migrationsDirectory: 'migrations',
rollbacksDirectory: 'rollbacks',
autoCreateRollbacks: true,
migrationsTableName: 'migrations',
}, null, 2));
}
if (!fs.existsSync('db/migrations')) {
fs.mkdirSync('db/migrations', { recursive: true });
const { rootDirectory, migrationsDirectory, rollbacksDirectory, autoCreateRollbacks, } = config();
if (!fs.existsSync(rootDirectory)) {
fs.mkdirSync(rootDirectory);
}
if (!fs.existsSync('db/rollbacks')) {
fs.mkdirSync('db/rollbacks');
if (!fs.existsSync(`${rootDirectory}/${migrationsDirectory}`)) {
fs.mkdirSync(`${rootDirectory}/${migrationsDirectory}`, {
recursive: true,
});
}
if (!fs.existsSync(`${rootDirectory}/${rollbacksDirectory}`) &&
autoCreateRollbacks) {
fs.mkdirSync(`${rootDirectory}/${rollbacksDirectory}`);
}
console.log(`
āœØ Initialized postgrate & created \`db\` folder at root of project directory! āœØ
āœØ Initialized postgrate & created \`${rootDirectory}\` folder at root of project directory! āœØ
`);
}

@@ -0,4 +1,6 @@

import config from '../config.js';
import pool from '../modules/pool.module.js';
export default async function () {
const { rows } = await pool.query('SELECT name, id, created_at FROM migrations ORDER BY id DESC');
const { migrationsTableName } = config();
const { rows } = await pool.query(`SELECT name, id, created_at FROM ${migrationsTableName} ORDER BY id DESC`);
if (!rows.length) {

@@ -5,0 +7,0 @@ console.log('\nNo migrations found!\n');

import fs from 'fs';
import init from './init.command.js';
import config from '../config.js';
export default function (name) {

@@ -8,7 +9,18 @@ if (!name) {

}
const fileName = generateMigrationFileName(name);
const migrationFilePath = `db/migrations/${fileName}`;
const rollbackFilePath = `db/rollbacks/rb-${fileName}`;
const { rootDirectory, migrationsDirectory, rollbacksDirectory, autoCreateRollbacks, migrationsTableName, } = config();
const fileName = generateMigrationFileName({
name,
config: {
rootDirectory,
migrationsDirectory,
rollbacksDirectory,
autoCreateRollbacks,
migrationsTableName,
},
});
const migrationFilePath = `${rootDirectory}/${migrationsDirectory}/${fileName}`;
const rollbackFilePath = `${rootDirectory}/${rollbacksDirectory}/rb-${fileName}`;
fs.writeFileSync(migrationFilePath, '');
fs.writeFileSync(rollbackFilePath, '');
if (autoCreateRollbacks)
fs.writeFileSync(rollbackFilePath, '');
console.log(`

@@ -18,4 +30,4 @@ Migration + rollback file created: ${fileName}

}
function generateMigrationFileName(name) {
if (!fs.existsSync('db/migrations')) {
function generateMigrationFileName({ name, config, }) {
if (!fs.existsSync(`${config.rootDirectory}/${config.migrationsDirectory}`)) {
init();

@@ -22,0 +34,0 @@ }

import fs from 'fs/promises';
import fsSync from 'fs';
import { confirmation, pool } from '../modules/index.js';
import Config from '../config.js';
export default async function (migrationId) {
validate(migrationId);
const name = await getMigrationRecordName(migrationId);
checkRollbackFileExists(name);
const config = Config();
const name = await getMigrationRecordName({ id: migrationId, config });
checkRollbackFileExists({ name, config });
await confirmation(name);
await rollback({ name, migrationId });
await rollback({ name, migrationId, config });
pool.end();

@@ -18,6 +20,4 @@ }

}
async function getMigrationRecordName(id) {
const { rows } = await pool.query('SELECT * FROM migrations WHERE id = $1', [
id,
]);
async function getMigrationRecordName({ id, config, }) {
const { rows } = await pool.query(`SELECT * FROM ${config.migrationsTableName} WHERE id = $1`, [id]);
if (!rows.length) {

@@ -29,4 +29,4 @@ console.error(`\nMigration with id ${id} not found!\n`);

}
function checkRollbackFileExists(name) {
if (!fsSync.existsSync(`db/rollbacks/rb-${name}`)) {
function checkRollbackFileExists({ name, config: { rootDirectory, rollbacksDirectory }, }) {
if (!fsSync.existsSync(`${rootDirectory}/${rollbacksDirectory}/rb-${name}`)) {
console.error(`Rollback file ${name} does not exist`);

@@ -36,7 +36,9 @@ process.exit(1);

}
async function rollback({ name, migrationId }) {
const rollback = await fs.readFile(`db/rollbacks/rb-${name}`, 'utf-8');
async function rollback({ name, migrationId, config: { rootDirectory, migrationsTableName, rollbacksDirectory }, }) {
const rollback = await fs.readFile(`${rootDirectory}/${rollbacksDirectory}/rb-${name}`, 'utf-8');
await pool.query(rollback);
await pool.query('DELETE FROM migrations WHERE id = $1', [migrationId]);
await pool.query(`DELETE FROM ${migrationsTableName} WHERE id = $1`, [
migrationId,
]);
console.log(`\nMigration ${name} rolled back\n`);
}

@@ -5,6 +5,8 @@ import fs from 'fs/promises';

import { confirmation, pool } from '../modules/index.js';
import Config from '../config.js';
export default async function () {
await createMigrationTable();
await fetchHistoricMigrations();
await runMigrations();
const config = Config();
await createMigrationTable(config);
await fetchHistoricMigrations(config);
await runMigrations(config);
return pool.end();

@@ -14,4 +16,5 @@ }

const historicMigrations = new Set();
const migrationsTableQuery = `
CREATE TABLE IF NOT EXISTS migrations (
async function createMigrationTable({ migrationsTableName, }) {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${migrationsTableName} (
id SERIAL PRIMARY KEY,

@@ -22,30 +25,28 @@ name VARCHAR(255) NOT NULL,

);
`;
async function createMigrationTable() {
await pool.query(migrationsTableQuery);
`);
}
async function fetchHistoricMigrations() {
const { rows } = await pool.query('SELECT name FROM migrations');
async function fetchHistoricMigrations({ migrationsTableName, }) {
const { rows } = await pool.query(`SELECT name FROM ${migrationsTableName}`);
rows.forEach((row) => historicMigrations.add(row.name));
}
async function getMigrationFileNames() {
if (!fsSync.existsSync('db/migrations')) {
async function getMigrationFileNames({ rootDirectory, migrationsDirectory, }) {
if (!fsSync.existsSync(`${rootDirectory}/${migrationsDirectory}`)) {
init();
}
const files = await fs.readdir('db/migrations');
const files = await fs.readdir(`${rootDirectory}/${migrationsDirectory}`);
return files.sort((a, b) => parseInt(a.split('-')[0]) - parseInt(b.split('-')[0]));
}
async function determinePendingMigrations() {
const files = await getMigrationFileNames();
async function determinePendingMigrations(config) {
const files = await getMigrationFileNames(config);
const pendingMigrations = files.filter((file) => !historicMigrations.has(file));
return pendingMigrations;
}
async function runMigration(migration) {
const filePath = `db/migrations/${migration}`;
async function runMigration({ migration, config: { rootDirectory, migrationsDirectory, migrationsTableName }, }) {
const filePath = `${rootDirectory}/${migrationsDirectory}/${migration}`;
const file = await fs.readFile(filePath, 'utf-8');
await client.query(file);
const { rows } = await client.query('INSERT INTO migrations (name) VALUES ($1) RETURNING id;', [migration]);
const { rows } = await client.query(`INSERT INTO ${migrationsTableName} (name) VALUES ($1) RETURNING id;`, [migration]);
console.log(`\tMigration ${migration} [id: ${rows[0].id}] has been executed šŸš€`);
}
async function transact({ pendingMigrations, }) {
async function transact({ pendingMigrations, config, }) {
try {

@@ -55,3 +56,3 @@ await client.query('BEGIN');

for (const migration of pendingMigrations) {
await runMigration(migration);
await runMigration({ migration, config });
}

@@ -73,4 +74,4 @@ console.log('\n');

}
async function runMigrations() {
const pendingMigrations = await determinePendingMigrations();
async function runMigrations(config) {
const pendingMigrations = await determinePendingMigrations(config);
if (!pendingMigrations.length) {

@@ -82,3 +83,3 @@ console.log('\nNo migrations to run\n');

await confirmation(pendingMigrations);
await transact({ pendingMigrations });
await transact({ pendingMigrations, config });
}
#!/usr/bin/env node
import { help, init, list, make, rollback, run } from './commands/index.js';
import { parser } from './modules/index.js';
const args = process.argv.slice(2);
const [command, second] = args;
switch (command) {
case '-i':
case 'init':
init();
break;
case '-m':
case 'make':
make(second);
break;
case '-r':
case 'run':
await run();
break;
case '-rb':
case 'rollback':
await rollback(second);
break;
case '-l':
case 'list':
await list();
break;
case '-h':
case 'help':
help();
break;
default:
console.log('Invalid command');
help();
process.exit(1);
break;
}
await parser({ command, second });
process.exit(0);
export { default as pool } from './pool.module.js';
export { default as confirmation } from './confirmation.module.js';
export { default as parser } from './parser.module.js';

@@ -6,3 +6,3 @@ {

},
"version": "1.0.1",
"version": "1.1.0",
"exports": "./dist/bin/index.js",

@@ -22,3 +22,4 @@ "description": "Simple PostgreSQL migration tool for raw-SQL loving developers.",

"build": "tsc",
"pretty": "prettier . --write"
"pretty": "prettier . --write",
"test": "vitest"
},

@@ -36,3 +37,3 @@ "bin": {

"typescript": "^5.3.2",
"vitest": "^0.34.6"
"vitest": "^1.4.0"
},

@@ -39,0 +40,0 @@ "peerDependencies": {

# Postgrate šŸ˜
A simple, intuitive postgres migration tool for
Welcome to Postgrate! This documentation provides a guide to using Postgrate, a
simple, intuitive postgres migration tool for
[node-postgres](https://node-postgres.com/)!

@@ -28,3 +29,4 @@

Once that's done, you can run the following command:
After installation, run the following command to initialize Postgrate in your
project directory and create necessary configuration files:

@@ -39,4 +41,4 @@ ```bash

command at least once before running the `rollback` command, otherwise you will
encounter an error. The `init` command has been included to accomodate future
plans to support a configuration file.\*
encounter an error. Running the `init` command will create a `.postgraterc` file
with the same defaults as though the `init` command were never run.\*

@@ -71,8 +73,135 @@ To create a migration, run:

## Things to Note
## Configuration
This package will create a table in your database called `migrations`. While we
hope to make this configurable in the future, for now this package will confilct
with any concurrently used package that follows a similar strategy.
As of version `1.1.0`, **Postgrate** supports the use of a configuration file!
In the current release, a `.postgraterc` file in the root of your project
directory written in `JSON` is the supported format.
Here is an example configuration file with the package defaults:
```json
{
"rootDirectory": "db",
"migrationsDirectory": "migrations",
"rollbackDirectory": "rollbacks",
"autoCreateRollbacks": true,
"migrationsTableName": "migrations"
}
```
Although detailed, option names have been chosen to facilitate intuitive
understanding of what each parameter does. That said, for completeness' sake,
here are some details about each config option:
## Configuration Options
- rootDirectory: Override the default directory name.
- migrationsDirectory: Override the default migrations directory name.
- rollbackDirectory: Override the default rollbacks directory name.
- autoCreateRollbacks: Set to true to automatically create rollback files.
- migrationsTableName: Name of the table created in your database.
#
### `rootDirectory`
The `rootDirectory` option allows you to override the default `db` directory
name that **Postgrate** creates at the root of your project.
**E.g.**
```json
"rootDirectory": "database" // then run `$ postgrate make <your-migration-name>`
```
Output:
```bash
database
|_migrations
|_<timestamp>-<your-migration-name>.sql
|_rollbacks
|_rb-<timestamp>-<your-migration-name>.sql
```
#
### `migrationsDirectory`
The `migrationsDirectory` option allows you to override the default `migrations`
directory name that **Postgrate** creates in the `db` (or specified as above)
directory.
**E.g.**
```json
"migrationsDirectory": "mg"
```
Output:
```bash
db
|_mg
|_<timestamp>-<your-migration-name>.sql
|_rollbacks
|_rb-<timestamp>-<your-migration-name>.sql
```
#
### `rollbacksDirectory`
The `rollbacksDirectory` option allows you to override the default `rollbacks`
directory name that **Postgrate** creates in the `db` (or specified as above)
directory.
**E.g.**
```json
"rollbacksDirectory": "rb"
```
Output:
```bash
db
|_migrations
|_<timestamp>-<your-migration-name>.sql
|_rb
|_rb-<timestamp>-<your-migration-name>.sql
```
#
### `autoCreateRollbacks`
The `autoCreateRollbacks` option can either be `true` or `false`. When set to
`false` a `rollbacks` directory will not be created, regardless of whether the
`rollbacksDirectory` option is set to a custom value.
Note that in this case, should you wish to create a rollback and use the
rollback command, rollback files will have to be created manually following the
formula shown in the above examples, repeated here for convenience:
```
rb_<migration-timestamp>-<migration-name>.sql
```
Essentially, you will need to do the following:
- create a `db/rollbacks` directory: name it `rollbacks` or whatever value you
assigned to the `rollbacksDirectory` config parameter
- create a rollback file within the directory and name it `rb_` plus the name of
the migration file you wish to rollback
#
### `migrationsTableName`
The `migrationsTableName` option allows you to set a cusom table name in which
to store migration records. Make sure that this name does not conflict with
other tables in your database. Once set, there is currently no way to update
this configuration option within a project.
## Commands

@@ -79,0 +208,0 @@

import fs from 'fs';
export default function (): void {
if (!fs.existsSync('db')) {
fs.mkdirSync('db');
import config from '../config.js';
export default function (createConfigFile = false): void {
if (createConfigFile) {
fs.writeFileSync(
'.postgraterc',
JSON.stringify(
{
rootDirectory: 'db',
migrationsDirectory: 'migrations',
rollbacksDirectory: 'rollbacks',
autoCreateRollbacks: true,
migrationsTableName: 'migrations',
},
null,
2,
),
);
}
if (!fs.existsSync('db/migrations')) {
fs.mkdirSync('db/migrations', { recursive: true });
const {
rootDirectory,
migrationsDirectory,
rollbacksDirectory,
autoCreateRollbacks,
} = config();
if (!fs.existsSync(rootDirectory)) {
fs.mkdirSync(rootDirectory);
}
if (!fs.existsSync('db/rollbacks')) {
fs.mkdirSync('db/rollbacks');
if (!fs.existsSync(`${rootDirectory}/${migrationsDirectory}`)) {
fs.mkdirSync(`${rootDirectory}/${migrationsDirectory}`, {
recursive: true,
});
}
if (
!fs.existsSync(`${rootDirectory}/${rollbacksDirectory}`) &&
autoCreateRollbacks
) {
fs.mkdirSync(`${rootDirectory}/${rollbacksDirectory}`);
}
console.log(`
āœØ Initialized postgrate & created \`db\` folder at root of project directory! āœØ
āœØ Initialized postgrate & created \`${rootDirectory}\` folder at root of project directory! āœØ
`);
}

@@ -0,6 +1,9 @@

import config from '../config.js';
import pool from '../modules/pool.module.js';
export default async function () {
const { migrationsTableName } = config();
const { rows } = await pool.query(
'SELECT name, id, created_at FROM migrations ORDER BY id DESC',
`SELECT name, id, created_at FROM ${migrationsTableName} ORDER BY id DESC`,
);

@@ -7,0 +10,0 @@

import fs from 'fs';
import init from './init.command.js';
import config, { IConfig } from '../config.js';

@@ -10,8 +11,26 @@ export default function (name: string): void {

const fileName = generateMigrationFileName(name);
const migrationFilePath = `db/migrations/${fileName}`;
const rollbackFilePath = `db/rollbacks/rb-${fileName}`;
const {
rootDirectory,
migrationsDirectory,
rollbacksDirectory,
autoCreateRollbacks,
migrationsTableName,
} = config();
const fileName = generateMigrationFileName({
name,
config: {
rootDirectory,
migrationsDirectory,
rollbacksDirectory,
autoCreateRollbacks,
migrationsTableName,
},
});
const migrationFilePath = `${rootDirectory}/${migrationsDirectory}/${fileName}`;
const rollbackFilePath = `${rootDirectory}/${rollbacksDirectory}/rb-${fileName}`;
fs.writeFileSync(migrationFilePath, '');
fs.writeFileSync(rollbackFilePath, '');
if (autoCreateRollbacks) fs.writeFileSync(rollbackFilePath, '');

@@ -25,4 +44,10 @@ console.log(

function generateMigrationFileName(name: string): string {
if (!fs.existsSync('db/migrations')) {
function generateMigrationFileName({
name,
config,
}: {
name: string;
config: IConfig;
}): string {
if (!fs.existsSync(`${config.rootDirectory}/${config.migrationsDirectory}`)) {
init();

@@ -29,0 +54,0 @@ }

import fs from 'fs/promises';
import fsSync from 'fs';
import { confirmation, pool } from '../modules/index.js';
import Config, { IConfig } from '../config.js';
export default async function (migrationId: string) {
validate(migrationId);
const name = await getMigrationRecordName(migrationId);
checkRollbackFileExists(name);
const config = Config();
const name = await getMigrationRecordName({ id: migrationId, config });
checkRollbackFileExists({ name, config });
await confirmation(name);
await rollback({ name, migrationId });
await rollback({ name, migrationId, config });
pool.end();

@@ -17,2 +19,3 @@ }

migrationId: string;
config: IConfig;
}

@@ -27,6 +30,13 @@

async function getMigrationRecordName(id: string): Promise<string> {
const { rows } = await pool.query('SELECT * FROM migrations WHERE id = $1', [
id,
]);
async function getMigrationRecordName({
id,
config,
}: {
id: string;
config: IConfig;
}): Promise<string> {
const { rows } = await pool.query(
`SELECT * FROM ${config.migrationsTableName} WHERE id = $1`,
[id],
);

@@ -41,4 +51,10 @@ if (!rows.length) {

function checkRollbackFileExists(name: string): void {
if (!fsSync.existsSync(`db/rollbacks/rb-${name}`)) {
function checkRollbackFileExists({
name,
config: { rootDirectory, rollbacksDirectory },
}: {
name: string;
config: IConfig;
}): void {
if (!fsSync.existsSync(`${rootDirectory}/${rollbacksDirectory}/rb-${name}`)) {
console.error(`Rollback file ${name} does not exist`);

@@ -49,7 +65,16 @@ process.exit(1);

async function rollback({ name, migrationId }: IRollback): Promise<void> {
const rollback = await fs.readFile(`db/rollbacks/rb-${name}`, 'utf-8');
async function rollback({
name,
migrationId,
config: { rootDirectory, migrationsTableName, rollbacksDirectory },
}: IRollback): Promise<void> {
const rollback = await fs.readFile(
`${rootDirectory}/${rollbacksDirectory}/rb-${name}`,
'utf-8',
);
await pool.query(rollback);
await pool.query('DELETE FROM migrations WHERE id = $1', [migrationId]);
await pool.query(`DELETE FROM ${migrationsTableName} WHERE id = $1`, [
migrationId,
]);
console.log(`\nMigration ${name} rolled back\n`);
}

@@ -5,7 +5,9 @@ import fs from 'fs/promises';

import { confirmation, pool } from '../modules/index.js';
import Config, { IConfig } from '../config.js';
export default async function () {
await createMigrationTable();
await fetchHistoricMigrations();
await runMigrations();
const config = Config();
await createMigrationTable(config);
await fetchHistoricMigrations(config);
await runMigrations(config);

@@ -18,4 +20,7 @@ return pool.end();

const migrationsTableQuery = `
CREATE TABLE IF NOT EXISTS migrations (
async function createMigrationTable({
migrationsTableName,
}: IConfig): Promise<void> {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${migrationsTableName} (
id SERIAL PRIMARY KEY,

@@ -26,18 +31,20 @@ name VARCHAR(255) NOT NULL,

);
`;
async function createMigrationTable(): Promise<void> {
await pool.query(migrationsTableQuery);
`);
}
async function fetchHistoricMigrations(): Promise<void> {
const { rows } = await pool.query('SELECT name FROM migrations');
async function fetchHistoricMigrations({
migrationsTableName,
}: IConfig): Promise<void> {
const { rows } = await pool.query(`SELECT name FROM ${migrationsTableName}`);
rows.forEach((row) => historicMigrations.add(row.name));
}
async function getMigrationFileNames(): Promise<string[]> {
if (!fsSync.existsSync('db/migrations')) {
async function getMigrationFileNames({
rootDirectory,
migrationsDirectory,
}: IConfig): Promise<string[]> {
if (!fsSync.existsSync(`${rootDirectory}/${migrationsDirectory}`)) {
init();
}
const files = await fs.readdir('db/migrations');
const files = await fs.readdir(`${rootDirectory}/${migrationsDirectory}`);
return files.sort(

@@ -48,4 +55,4 @@ (a, b) => parseInt(a.split('-')[0]) - parseInt(b.split('-')[0]),

async function determinePendingMigrations(): Promise<string[]> {
const files = await getMigrationFileNames();
async function determinePendingMigrations(config: IConfig): Promise<string[]> {
const files = await getMigrationFileNames(config);
const pendingMigrations = files.filter(

@@ -57,8 +64,14 @@ (file) => !historicMigrations.has(file),

async function runMigration(migration: string): Promise<void> {
const filePath = `db/migrations/${migration}`;
async function runMigration({
migration,
config: { rootDirectory, migrationsDirectory, migrationsTableName },
}: {
migration: string;
config: IConfig;
}): Promise<void> {
const filePath = `${rootDirectory}/${migrationsDirectory}/${migration}`;
const file = await fs.readFile(filePath, 'utf-8');
await client.query(file);
const { rows } = await client.query(
'INSERT INTO migrations (name) VALUES ($1) RETURNING id;',
`INSERT INTO ${migrationsTableName} (name) VALUES ($1) RETURNING id;`,
[migration],

@@ -73,4 +86,6 @@ );

pendingMigrations,
config,
}: {
pendingMigrations: string[];
config: IConfig;
}): Promise<void> {

@@ -82,3 +97,3 @@ try {

for (const migration of pendingMigrations) {
await runMigration(migration);
await runMigration({ migration, config });
}

@@ -100,4 +115,4 @@

async function runMigrations(): Promise<void> {
const pendingMigrations = await determinePendingMigrations();
async function runMigrations(config: IConfig): Promise<void> {
const pendingMigrations = await determinePendingMigrations(config);

@@ -111,3 +126,3 @@ if (!pendingMigrations.length) {

await confirmation(pendingMigrations);
await transact({ pendingMigrations });
await transact({ pendingMigrations, config });
}
#!/usr/bin/env node
import { help, init, list, make, rollback, run } from './commands/index.js';
import { parser } from './modules/index.js';

@@ -7,40 +7,4 @@ const args = process.argv.slice(2);

switch (command) {
case '-i':
case 'init':
init();
break;
await parser({ command, second });
case '-m':
case 'make':
make(second);
break;
case '-r':
case 'run':
await run();
break;
case '-rb':
case 'rollback':
await rollback(second);
break;
case '-l':
case 'list':
await list();
break;
case '-h':
case 'help':
help();
break;
default:
console.log('Invalid command');
help();
process.exit(1);
break;
}
process.exit(0);
export { default as pool } from './pool.module.js';
export { default as confirmation } from './confirmation.module.js';
export { default as parser } from './parser.module.js';
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with āš”ļø by Socket Inc