Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue-163: added ability to include external files in current.sql. #195

Merged
merged 32 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a9828ad
issue-163: added ability to include external files in current.sql.
jnbarlow Dec 16, 2023
c818a03
Update README.md
jnbarlow Dec 22, 2023
62982ca
Update README.md
jnbarlow Dec 22, 2023
8abc06f
Update README.md
jnbarlow Dec 22, 2023
1f2013f
Update README.md
jnbarlow Dec 22, 2023
e676a1e
Update README.md
jnbarlow Dec 22, 2023
d4d15ac
Update README.md
jnbarlow Dec 22, 2023
90da00f
Update __tests__/compile.test.ts
jnbarlow Dec 22, 2023
f676d3a
Update __tests__/compile.test.ts
jnbarlow Dec 22, 2023
b47053c
Update __tests__/compile.test.ts
jnbarlow Dec 22, 2023
c41cc22
Fixed up include code to use a fixed directory, and recursively include.
jnbarlow Dec 22, 2023
75e0203
Added fsrealpath support, still working on tests.
jnbarlow Dec 22, 2023
cbaa1b5
issue-163: got throwing test fixed
jnbarlow Dec 29, 2023
710ff4f
Issue-163: fixed broken tests after adding includes to readCurrentMig…
jnbarlow Dec 30, 2023
e9483cd
issue-163: added fixtures directory to watch
jnbarlow Dec 30, 2023
40d90f8
issue-163: lint fix
jnbarlow Dec 30, 2023
83c4f85
Update src/current.ts
jnbarlow Jan 3, 2024
b7fed69
Update src/current.ts
jnbarlow Jan 3, 2024
ce63096
issue-163: added globstar to fixtures watch.
jnbarlow Jan 3, 2024
b2f2b6e
issue-163: fixed watch glob, fixed premature regex escaping of content.
jnbarlow Jan 4, 2024
bddfcce
Wider 'pg' range
benjie Jan 18, 2024
82a2315
Using variable now
benjie Jan 18, 2024
68d4fbd
Refactor the include code
benjie Jan 18, 2024
da4bed9
Fix lint issues
benjie Jan 18, 2024
642be0d
Fix whitespace bug
benjie Jan 18, 2024
e559845
Fix test and enforce full stack trace
benjie Jan 18, 2024
47d0b33
Better error tests
benjie Jan 18, 2024
b28b4fd
realPath the fixtures
benjie Jan 18, 2024
a1d9a02
No need to glob
benjie Jan 18, 2024
8c0ad1b
Oops, missed array
benjie Jan 18, 2024
f7768ab
Improve error further
benjie Jan 18, 2024
062003f
Neater imports
benjie Jan 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,59 @@ by `graphile-migrate watch` is defined. By default this is in the
`migrations/current.sql` file, but it might be `migrations/current/*.sql` if
you're using folder mode.

#### Including external files in the current migration

You can include external files in your `current.sql` to better assist in source
control. These includes are identified by paths within the `migrations/fixtures`
folder.

For example. Given the following directory structure:

```
/- migrate
- migrations
|
- current.sql
- fixtures
|
- functions
|
- myfunction.sql
```

and the contents of `myfunction.sql`:

```sql
create or replace function myfunction(a int, b int)
returns int as $$
select a + b;
$$ language sql stable;
```

When you make changes to `myfunction.sql`, include it in your current migration
by adding `--!include functions/myfunction.sql` to your `current.sql` (or any
`current/*.sql`). This statement doesn't need to be at the top of the file,
wherever it is will be replaced by the content of
`migrations/fixtures/functions/myfunction.sql` when the migration is committed.

```sql
--!include fixtures/functions/myfunction.sql
drop policy if exists access_by_numbers on mytable;
create policy access_by_numbers on mytable for update using (myfunction(4, 2) < 42);
```

and when the migration is committed or watched, the contents of `myfunction.sql`
will be included in the result, such that the following SQL is executed:

```sql
create or replace function myfunction(a int, b int)
returns int as $$
select a + b;
$$ language sql stable;
drop policy if exists access_by_numbers on mytable;
create policy access_by_numbers on mytable for update using (myfunction(4, 2) < 42);
```

### Committed migration(s)

The files for migrations that you've committed with `graphile-migrate commit`
Expand Down
9 changes: 9 additions & 0 deletions __tests__/__snapshots__/include.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`compiles an included file, and won't get stuck in an infinite include loop 1`] = `
"Circular include detected - '~/migrations/fixtures/foo.sql' is included again! Import statement: \`--!include foo.sql\`; trace:
~/migrations/fixtures/foo.sql
~/migrations/current.sql"
`;

exports[`disallows calling files outside of the migrations/fixtures folder 1`] = `"Forbidden: cannot include path '~/outsideFolder/foo.sql' because it's not inside '~/migrations/fixtures'"`;
2 changes: 1 addition & 1 deletion __tests__/commit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./helpers"; // Has side-effects; must come first

import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import mockFs from "mock-fs";

import { commit } from "../src";
Expand Down
7 changes: 6 additions & 1 deletion __tests__/compile.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import "./helpers";

import { compile } from "../src";
import * as mockFs from "mock-fs";

import { compile } from "../src";
let old: string | undefined;
beforeAll(() => {
old = process.env.DATABASE_AUTHENTICATOR;
Expand All @@ -11,6 +12,10 @@ afterAll(() => {
process.env.DATABASE_AUTHENTICATOR = old;
});

afterEach(() => {
mockFs.restore();
});

it("compiles SQL with settings", async () => {
expect(
await compile(
Expand Down
145 changes: 145 additions & 0 deletions __tests__/include.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import "./helpers";

import mockFs from "mock-fs";

import { compileIncludes } from "../src/migration";
import { ParsedSettings, parseSettings } from "../src/settings";

let old: string | undefined;
let settings: ParsedSettings;
beforeAll(async () => {
old = process.env.DATABASE_AUTHENTICATOR;
process.env.DATABASE_AUTHENTICATOR = "dbauth";
settings = await parseSettings({
connectionString: "postgres://dbowner:dbpassword@dbhost:1221/dbname",
placeholders: {
":DATABASE_AUTHENTICATOR": "!ENV",
},
migrationsFolder: "migrations",
});
});
afterAll(() => {
process.env.DATABASE_AUTHENTICATOR = old;
});

afterEach(() => {
mockFs.restore();
});

/** Pretents that our compiled files are 'current.sql' */
const FAKE_VISITED = new Set([`${process.cwd()}/migrations/current.sql`]);

it("compiles an included file", async () => {
mockFs({
"migrations/fixtures/foo.sql": "select * from foo;",
});
expect(
await compileIncludes(
settings,
`\
--!include foo.sql
`,
FAKE_VISITED,
),
).toEqual(`\
select * from foo;
`);
});

it("compiles multiple included files", async () => {
mockFs({
"migrations/fixtures/dir1/foo.sql": "select * from foo;",
"migrations/fixtures/dir2/bar.sql": "select * from bar;",
"migrations/fixtures/dir3/baz.sql": "--!include dir4/qux.sql",
"migrations/fixtures/dir4/qux.sql": "select * from qux;",
});
expect(
await compileIncludes(
settings,
`\
--!include dir1/foo.sql
--!include dir2/bar.sql
--!include dir3/baz.sql
`,
FAKE_VISITED,
),
).toEqual(`\
select * from foo;
select * from bar;
select * from qux;
`);
});

it("compiles an included file, and won't get stuck in an infinite include loop", async () => {
mockFs({
"migrations/fixtures/foo.sql": "select * from foo;\n--!include foo.sql",
});
const promise = compileIncludes(
settings,
`\
--!include foo.sql
`,
FAKE_VISITED,
);
await expect(promise).rejects.toThrowError(/Circular include/);
const message = await promise.catch((e) => e.message);
expect(message.replaceAll(process.cwd(), "~")).toMatchSnapshot();
});

it("disallows calling files outside of the migrations/fixtures folder", async () => {
mockFs({
"migrations/fixtures/bar.sql": "",
"outsideFolder/foo.sql": "select * from foo;",
});

const promise = compileIncludes(
settings,
`\
--!include ../../outsideFolder/foo.sql
`,
FAKE_VISITED,
);
await expect(promise).rejects.toThrowError(/Forbidden: cannot include/);
const message = await promise.catch((e) => e.message);
expect(message.replaceAll(process.cwd(), "~")).toMatchSnapshot();
});

it("compiles an included file that contains escapable things", async () => {
mockFs({
"migrations/fixtures/foo.sql": `\
begin;

create or replace function current_user_id() returns uuid as $$
select nullif(current_setting('user.id', true)::text, '')::uuid;
$$ language sql stable;

comment on function current_user_id is E'The ID of the current user.';

grant all on function current_user_id to :DATABASE_USER;

commit;
`,
});
expect(
await compileIncludes(
settings,
`\
--!include foo.sql
`,
FAKE_VISITED,
),
).toEqual(`\
begin;

create or replace function current_user_id() returns uuid as $$
select nullif(current_setting('user.id', true)::text, '')::uuid;
$$ language sql stable;

comment on function current_user_id is E'The ID of the current user.';

grant all on function current_user_id to :DATABASE_USER;

commit;

`);
});
11 changes: 11 additions & 0 deletions __tests__/readCurrentMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ With multiple lines
const content = await readCurrentMigration(parsedSettings, currentLocation);
expect(content).toEqual(contentWithSplits);
});

it("reads from current.sql, and processes included files", async () => {
mockFs({
"migrations/current.sql": "--!include foo_current.sql",
"migrations/fixtures/foo_current.sql": "-- TEST from foo",
});

const currentLocation = await getCurrentMigrationLocation(parsedSettings);
const content = await readCurrentMigration(parsedSettings, currentLocation);
expect(content).toEqual("-- TEST from foo");
});
2 changes: 1 addition & 1 deletion __tests__/uncommit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./helpers"; // Has side-effects; must come first

import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import mockFs from "mock-fs";

import { commit, migrate, uncommit } from "../src";
Expand Down
2 changes: 1 addition & 1 deletion __tests__/writeCurrentMigration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./helpers"; // Has side-effects; must come first

import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import mockFs from "mock-fs";

import {
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
"dependencies": {
"@graphile/logger": "^0.2.0",
"@types/json5": "^2.2.0",
"@types/node": "^20.11.5",
"@types/pg": "^8.10.9",
"@types/node": "^18",
"@types/pg": ">=6 <9",
"chalk": "^4",
"chokidar": "^3.5.3",
"json5": "^2.2.3",
"pg": "^8.11.3",
"pg": ">=6.5 <9",
"pg-connection-string": "^2.6.2",
"pg-minify": "^1.6.3",
"tslib": "^2.6.2",
Expand Down
2 changes: 1 addition & 1 deletion scripts/update-docs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
const { promises: fsp } = require("fs");
const fsp = require("fs/promises");
const { spawnSync } = require("child_process");

async function main() {
Expand Down
4 changes: 4 additions & 0 deletions src/__mocks__/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ export const runStringMigration = jest.fn(
export const runCommittedMigration = jest.fn(
(_client, _settings, _context, _committedMigration, _logSuffix) => {},
);

export const compileIncludes = jest.fn((parsedSettings, content) => {
return content;
});
2 changes: 1 addition & 1 deletion src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Logger } from "@graphile/logger";
import { exec as rawExec } from "child_process";
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { parse } from "pg-connection-string";
import { inspect, promisify } from "util";

Expand Down
3 changes: 2 additions & 1 deletion src/commands/_common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { constants, promises as fsp } from "fs";
import { constants } from "fs";
import * as fsp from "fs/promises";
import * as JSON5 from "json5";
import { resolve } from "path";
import { parse } from "pg-connection-string";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/commit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pgMinify = require("pg-minify");
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { CommandModule } from "yargs";

import {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/compile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { CommandModule } from "yargs";

import { compilePlaceholders } from "../migration";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { CommandModule } from "yargs";

import { getCurrentMigrationLocation, writeCurrentMigration } from "../current";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { QueryResultRow } from "pg";
import { CommandModule } from "yargs";

Expand Down
2 changes: 1 addition & 1 deletion src/commands/uncommit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pgMinify = require("pg-minify");
import { promises as fsp } from "fs";
import * as fsp from "fs/promises";
import { CommandModule } from "yargs";

import {
Expand Down
Loading
Loading