-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: tags from config, and in log output (#100)
* tags config * tags from config? * filter out empties * remove duplicate prop * fix: only show tags for given oid Co-authored-by: Misha Kaletsky <[email protected]>
- Loading branch information
Showing
6 changed files
with
311 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,6 +112,7 @@ This query will return: | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
|
@@ -125,6 +126,7 @@ This query will return: | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "id", | ||
|
@@ -262,6 +264,7 @@ where identifier->>'id' = '1' | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
|
@@ -275,6 +278,7 @@ where identifier->>'id' = '1' | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "id", | ||
|
@@ -325,6 +329,7 @@ where id = 2 | |
"author": "Alice ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "id", | ||
|
@@ -366,6 +371,7 @@ where id = 201 | |
"author": "Bob ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "id", | ||
|
@@ -408,6 +414,7 @@ where id = 2 | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
|
@@ -443,6 +450,16 @@ set | |
where id = 3; | ||
``` | ||
|
||
Or, set them in git config as a colon-separated list: | ||
|
||
```sql | ||
select git_set_local_config('tags', 'your_app_request_id=1234:your_app_trace_id=5678'); | ||
|
||
update test_table | ||
set text = 'item 3 yet another value' | ||
where id = 3; | ||
``` | ||
|
||
### Restoring previous versions | ||
|
||
`git_resolve` gives you a json representation of a prior version of a row, which can be used for backup and restore. The first argument is a `git` json value, the second value is a valid git ref string (e.g. a git oid returned by `git_log`, or `HEAD`, or `main`. Note that an issue with [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git/issues/1238) means that you can't currently pass values like `HEAD~1` here). | ||
|
@@ -488,6 +505,91 @@ returning id, text | |
|
||
If you used `tags` as described above, you can take advantage of them to restore to a known-good state easily: | ||
|
||
```sql | ||
select git_log(git) | ||
from test_table | ||
where id = 3 | ||
``` | ||
|
||
```json | ||
[ | ||
{ | ||
"git_log": [ | ||
{ | ||
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table", | ||
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [ | ||
"your_app_request_id=1234", | ||
"your_app_trace_id=5678" | ||
], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
"new": "item 3 yet another value", | ||
"old": "item 3 new year value" | ||
} | ||
] | ||
}, | ||
{ | ||
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table", | ||
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [ | ||
"2001", | ||
"2001-01", | ||
"2001-01-01" | ||
], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
"new": "item 3 new year value", | ||
"old": "item 3 boxing day value" | ||
} | ||
] | ||
}, | ||
{ | ||
"message": "test_table_git_track_trigger: BEFORE UPDATE ROW on public.test_table", | ||
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [ | ||
"2000", | ||
"2000-12", | ||
"2000-12-26" | ||
], | ||
"changes": [ | ||
{ | ||
"field": "text", | ||
"new": "item 3 boxing day value", | ||
"old": "item 3 xmas day value" | ||
} | ||
] | ||
}, | ||
{ | ||
"message": "test_table_git_track_trigger: BEFORE INSERT ROW on public.test_table", | ||
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "id", | ||
"new": 3 | ||
}, | ||
{ | ||
"field": "text", | ||
"new": "item 3 xmas day value" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
] | ||
``` | ||
|
||
```sql | ||
update test_table set (id, text) = | ||
( | ||
|
@@ -508,6 +610,26 @@ returning id, text | |
} | ||
``` | ||
|
||
```sql | ||
update test_table set (id, text) = | ||
( | ||
select id, text | ||
from json_populate_record( | ||
null::test_table, | ||
git_resolve(git, ref := 'your_app_request_id=1234') | ||
) | ||
) | ||
where id = 3 | ||
returning id, text | ||
``` | ||
|
||
```json | ||
{ | ||
"id": 3, | ||
"text": "item 3 yet another value" | ||
} | ||
``` | ||
|
||
A similar technique can restore a deleted item: | ||
|
||
```sql | ||
|
@@ -571,6 +693,7 @@ where git = 'https://github.com/mmkal/plv8-git.git' | |
"author": "pguser ([email protected])", | ||
"timestamp": "2000-12-25T12:00:00.000Z", | ||
"oid": "[oid]", | ||
"tags": [], | ||
"changes": [ | ||
{ | ||
"field": "git", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import * as git from 'isomorphic-git' | |
import * as serializer from './serializer' | ||
import {PG_Vars} from './pg-types' | ||
import {setupMemfs} from './fs' | ||
import {memoizeAsync} from './memoize' | ||
|
||
function writeGitFiles(gitFiles: any, fs: memfs.IFs) { | ||
if (!gitFiles) { | ||
|
@@ -42,7 +43,7 @@ export const rowToRepo = ({OLD, NEW, ...pg}: PG_Vars) => { | |
|
||
const gitParams = NEW?.[repoColumn] || {} | ||
|
||
const commitMessage = `${pg.TG_NAME}: ${pg.TG_WHEN} ${pg.TG_OP} ${pg.TG_LEVEL} on ${pg.TG_TABLE_SCHEMA}.${pg.TG_TABLE_NAME}`.trim() | ||
const defaultCommitMessage = `${pg.TG_NAME}: ${pg.TG_WHEN} ${pg.TG_OP} ${pg.TG_LEVEL} on ${pg.TG_TABLE_SCHEMA}.${pg.TG_TABLE_NAME}`.trim() | ||
|
||
return Promise.resolve() | ||
.then(setupGitFolder) | ||
|
@@ -60,20 +61,31 @@ export const rowToRepo = ({OLD, NEW, ...pg}: PG_Vars) => { | |
.then(() => | ||
git.commit({ | ||
...repo, | ||
message: [gitParams.commit?.message, commitMessage].filter(Boolean).join('\n\n'), | ||
message: [ | ||
gitParams.commit?.message, | ||
getSetting('commit.message'), | ||
defaultCommitMessage, | ||
getSetting('commit.message.signature'), | ||
] | ||
.filter(Boolean) | ||
.join('\n\n'), | ||
author: { | ||
name: gitParams.commit?.author?.name || getSetting('user.name') || 'pguser', | ||
email: gitParams.commit?.author?.email || getSetting('user.email') || '[email protected]', | ||
}, | ||
}), | ||
) | ||
.then(commit => | ||
Promise.all( | ||
(gitParams.tags || []).map((tag: string) => { | ||
.then(commit => { | ||
const allTags: string[] = [ | ||
...(getSetting('tags')?.split(':') || []), // colon separated tags from config | ||
...(gitParams.tags || []), | ||
].filter(Boolean) | ||
return Promise.all( | ||
allTags.map((tag: string) => { | ||
return git.tag({...repo, ref: tag, object: commit}) | ||
}), | ||
), | ||
) | ||
) | ||
}) | ||
}) | ||
.then(() => { | ||
const files: Record<string, number[]> = {} | ||
|
@@ -98,7 +110,7 @@ declare const plv8: { | |
const getSetting = (name: string) => { | ||
// https://www.postgresql.org/docs/9.4/functions-admin.html | ||
const [{git_get_config}] = plv8.execute('select git_get_config($1)', [name]) | ||
return git_get_config | ||
return git_get_config as string | null | ||
} | ||
|
||
type TreeInfo = {type: string; content: string; oid: string} | ||
|
@@ -113,6 +125,10 @@ export const gitLog = (gitRepoJson: object, depth?: number) => { | |
const {fs} = setupMemfs() | ||
const repo = {fs, dir: '/repo'} | ||
|
||
// `listTags` lists all tags for the repo. so we need to use resolveRef to check that each tags is pointing at a given id | ||
// this can mean a lot of repeated calls. | ||
const resolveTagRef = memoizeAsync(git.resolveRef) | ||
|
||
return Promise.resolve() | ||
.then(() => writeGitFiles(gitRepoJson, fs)) | ||
.then(() => git.log({...repo, depth})) | ||
|
@@ -130,11 +146,20 @@ export const gitLog = (gitRepoJson: object, depth?: number) => { | |
) | ||
}, | ||
}) | ||
.then((results: WalkResult[]) => ({ | ||
.then((results: WalkResult[]) => { | ||
return git.listTags({...repo}).then(tags => { | ||
return Promise.all(tags.map(t => resolveTagRef({...repo, ref: t}))).then(resolvedTags => { | ||
const filteredTags = tags.filter((t, i) => resolvedTags[i] === e.oid) | ||
return {results, tags: filteredTags} | ||
}) | ||
}) | ||
}) | ||
.then(({results, tags}) => ({ | ||
message: e.commit.message.trim(), | ||
author: `${e.commit.author.name} (${e.commit.author.email})`, | ||
timestamp: new Date(e.commit.author.timestamp * 1000).toISOString(), | ||
oid: e.oid, | ||
tags, | ||
changes: results | ||
.filter( | ||
r => r.ChildInfo?.type === 'blob' && r.filepath !== '.' && r.ChildInfo.oid !== r.ParentInfo?.oid, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export const memoizeAsync = <A extends any[], T>(fn: (...args: A) => Promise<T>): ((...args: A) => Promise<T>) => { | ||
const cache = new Map<string, T>() | ||
return (...args: A) => { | ||
const key = JSON.stringify(args) | ||
if (cache.has(key)) { | ||
return Promise.resolve(cache.get(key)!) | ||
} | ||
|
||
return fn(...args).then(result => { | ||
cache.set(key, result) | ||
return result | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {memoizeAsync} from '../src/memoize' | ||
|
||
test('memoize', async () => { | ||
const mock = jest.fn(async () => Math.random()) | ||
|
||
const memoized = memoizeAsync(mock) | ||
|
||
const first = await memoized() | ||
const second = await memoized() | ||
|
||
expect([first, second]).toEqual([expect.any(Number), expect.any(Number)]) | ||
expect(mock).toHaveBeenCalledTimes(1) | ||
expect(second).toEqual(first) | ||
}) |
Oops, something went wrong.