sequel-pgt_outbox 0.2.0
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.
- checksums.yaml +7 -0
- data/.overcommit.yml +345 -0
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +31 -0
- data/Readme.adoc +82 -0
- data/ci/build_image.sh +257 -0
- data/ci/publish-gem.sh +77 -0
- data/lib/sequel/extensions/pgt_outbox.rb +17 -0
- data/lib/sequel/pgt_outbox/database_methods.rb +12 -0
- data/lib/sequel/pgt_outbox/function.rb +66 -0
- data/lib/sequel/pgt_outbox/table.rb +161 -0
- data/lib/sequel/pgt_outbox/trigger.rb +51 -0
- data/lib/sequel/pgt_outbox/version.rb +9 -0
- data/lib/sequel/pgt_outbox.rb +56 -0
- data/sig/sequel/pgt_outbox.rbs +6 -0
- metadata +78 -0
data/ci/build_image.sh
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
if readlink -f . >/dev/null 2>&1 # {{{ makes readlink work on mac
|
4
|
+
then
|
5
|
+
readlink=readlink
|
6
|
+
else
|
7
|
+
if greadlink -f . >/dev/null 2>&1
|
8
|
+
then
|
9
|
+
readlink=greadlink
|
10
|
+
else
|
11
|
+
printf "You must install greadlink to use this (brew install coreutils)\n" >&2
|
12
|
+
fi
|
13
|
+
fi # }}}
|
14
|
+
|
15
|
+
# Set here to the full path to this script
|
16
|
+
me=${BASH_SOURCE[0]}
|
17
|
+
[ -L "$me" ] && me=$($readlink -f "$me")
|
18
|
+
here=$(cd "$(dirname "$me")" && pwd)
|
19
|
+
just_me=$(basename "$me")
|
20
|
+
|
21
|
+
repo_top=$(git rev-parse --show-toplevel)
|
22
|
+
cd "$repo_top" || {
|
23
|
+
printf "Could not cd to %s\n" "$repo_top" >&2
|
24
|
+
exit 1
|
25
|
+
}
|
26
|
+
|
27
|
+
base_dir=$(basename "$(pwd)")
|
28
|
+
: "${UBUNTU_VERSION:=bookworm}"
|
29
|
+
: "${BUILD_CONTEXT:=$(pwd)}"
|
30
|
+
: "${IMAGE_NAME:=$base_dir}"
|
31
|
+
: "${LICENSE:=MIT}"
|
32
|
+
: "${REGISTRY:=ghcr.io}"
|
33
|
+
: "${RUBY_VERSION:=3.3.6}"
|
34
|
+
: "${REGISTRY_TOKEN:=$GITHUB_TOKEN}"
|
35
|
+
|
36
|
+
base_image_tag="$RUBY_VERSION-$UBUNTU_VERSION"
|
37
|
+
base_exists=$(skopeo list-tags docker://docker.io/ruby |jq -r "any(.Tags[] == \"$base_image_tag\"; .)")
|
38
|
+
if [ "$base_exists" = "false" ]
|
39
|
+
then
|
40
|
+
printf "Base image %s does not exist at docker.io/ruby, cannot build.\n" "$base_image_tag" >&2
|
41
|
+
exit 99
|
42
|
+
fi
|
43
|
+
|
44
|
+
usage() { # {{{
|
45
|
+
cat <<-EOT
|
46
|
+
Build an image, optionally pushing it to the registry
|
47
|
+
|
48
|
+
Usage: $0 <options> <image_tag>
|
49
|
+
Options:
|
50
|
+
-c CONTAINERFILE Path to the containerfile (default: ./oci/Containerfile)
|
51
|
+
-C CONTEXT Build context (default: $BUILD_CONTEXT)
|
52
|
+
-i NAME Name of the image (default: $IMAGE_NAME)
|
53
|
+
-l LICENSE License of the image (default: $LICENSE)
|
54
|
+
-r REGISTRY Registry to push the image to when -p is given (default: $REGISTRY)
|
55
|
+
-p Push the image to the registry
|
56
|
+
-h Show help / usage
|
57
|
+
EOT
|
58
|
+
} # }}}
|
59
|
+
|
60
|
+
die() { # {{{
|
61
|
+
local -i code
|
62
|
+
code=$1
|
63
|
+
shift
|
64
|
+
error "$*"
|
65
|
+
printf "\n" >&2
|
66
|
+
usage >&2
|
67
|
+
# shellcheck disable=SC2086
|
68
|
+
exit $code
|
69
|
+
} # }}}
|
70
|
+
|
71
|
+
## Logging functions # {{{
|
72
|
+
log() { # {{{
|
73
|
+
printf "%s [%s] <%s> %s\n" "$(date '+%Y-%m-%d %H:%M:%S.%6N')" "$$" "${just_me:-$0}" "$*"
|
74
|
+
} # }}}
|
75
|
+
|
76
|
+
debug() { # {{{
|
77
|
+
[ $verbose -lt 2 ] && return 0
|
78
|
+
# shellcheck disable=SC2059
|
79
|
+
log_line=$(printf "$@")
|
80
|
+
log "[DEBUG] $log_line" >&2
|
81
|
+
} # }}}
|
82
|
+
|
83
|
+
warn() { # {{{
|
84
|
+
# shellcheck disable=SC2059
|
85
|
+
log_line=$(printf "$@")
|
86
|
+
log "[WARN] $log_line" >&2
|
87
|
+
} # }}}
|
88
|
+
|
89
|
+
error() { # {{{
|
90
|
+
# shellcheck disable=SC2059
|
91
|
+
log_line=$(printf "$@")
|
92
|
+
log "[ERROR] $log_line" >&2
|
93
|
+
} # }}}
|
94
|
+
|
95
|
+
info() { # {{{
|
96
|
+
[ $verbose -lt 1 ] && return 0
|
97
|
+
# shellcheck disable=SC2059
|
98
|
+
log_line=$(printf "$@")
|
99
|
+
log "[INFO] $log_line" >&2
|
100
|
+
} # }}}
|
101
|
+
# }}}
|
102
|
+
|
103
|
+
push=0
|
104
|
+
verbose=0
|
105
|
+
while getopts :hpvc:C:i:l:r: opt # {{{
|
106
|
+
do
|
107
|
+
case $opt in
|
108
|
+
c)
|
109
|
+
CONTAINERFILE=$OPTARG
|
110
|
+
;;
|
111
|
+
C)
|
112
|
+
BUILD_CONTEXT=$OPTARG
|
113
|
+
;;
|
114
|
+
i)
|
115
|
+
IMAGE_NAME=$OPTARG
|
116
|
+
;;
|
117
|
+
l)
|
118
|
+
LICENSE=$OPTARG
|
119
|
+
;;
|
120
|
+
r)
|
121
|
+
REGISTRY=$OPTARG
|
122
|
+
;;
|
123
|
+
p)
|
124
|
+
push=1
|
125
|
+
;;
|
126
|
+
v)
|
127
|
+
verbose=$((verbose + 1))
|
128
|
+
;;
|
129
|
+
h)
|
130
|
+
usage
|
131
|
+
exit
|
132
|
+
;;
|
133
|
+
:)
|
134
|
+
printf "Option %s requires an argument\n" "$OPTARG" >&2
|
135
|
+
usage >&2
|
136
|
+
exit 28
|
137
|
+
;;
|
138
|
+
?)
|
139
|
+
printf "Invalid option '%s'\n" "$OPTARG" >&2
|
140
|
+
usage >&2
|
141
|
+
exit 27
|
142
|
+
;;
|
143
|
+
esac
|
144
|
+
done # }}}
|
145
|
+
shift $((OPTIND-1))
|
146
|
+
|
147
|
+
tag=$1
|
148
|
+
[ -z "$tag" ] && die 1 "Missing image tag"
|
149
|
+
shift
|
150
|
+
|
151
|
+
# Check for extra argument
|
152
|
+
if [ $# -gt 0 ]; then
|
153
|
+
# If we have the special argument '--' we shift it away, otherwise we die
|
154
|
+
[ "$1" != '--' ] && die 2 "Too many arguments"
|
155
|
+
# Once this is shifted away, the rest of the arguments are passed to the build command, below
|
156
|
+
shift
|
157
|
+
fi
|
158
|
+
|
159
|
+
if [ -z "$CONTAINERFILE" ]; then
|
160
|
+
printf "No containerfile specified, looking for default locations\n"
|
161
|
+
for containerfile in Containerfile Dockerfile
|
162
|
+
do
|
163
|
+
if [ -f ./oci/"$containerfile" ]; then
|
164
|
+
debug "Found ./oci/%s\n" "$containerfile"
|
165
|
+
containerfile=./oci/"$containerfile"
|
166
|
+
break
|
167
|
+
fi
|
168
|
+
if [ -f "$containerfile" ]; then
|
169
|
+
debug "Found %s\n" "$containerfile"
|
170
|
+
break
|
171
|
+
fi
|
172
|
+
done
|
173
|
+
else
|
174
|
+
[ -f "$CONTAINERFILE" ] || die 3 "Containerfile '$CONTAINERFILE' not found"
|
175
|
+
debug "Using containerfile %s\n" "$CONTAINERFILE"
|
176
|
+
containerfile=$CONTAINERFILE
|
177
|
+
fi
|
178
|
+
|
179
|
+
[ -f "$containerfile" ] || die 4 "No containerfile found"
|
180
|
+
|
181
|
+
[ -d "$BUILD_CONTEXT" ] || die 5 "Build context '$BUILD_CONTEXT' not found"
|
182
|
+
|
183
|
+
debug 'Building image from %s in in %s\n' "$containerfile" "$here"
|
184
|
+
# Build the image
|
185
|
+
if command -v podman 2>/dev/null
|
186
|
+
then
|
187
|
+
runtime=podman
|
188
|
+
elif command -v docker 2>/dev/null
|
189
|
+
then
|
190
|
+
runtime=docker
|
191
|
+
else
|
192
|
+
die 6 "No container runtime found"
|
193
|
+
fi
|
194
|
+
|
195
|
+
revision=$(git rev-parse HEAD)
|
196
|
+
shortref=$(git rev-parse --short "$revision")
|
197
|
+
repo_url=$(git remote get-url upstream)
|
198
|
+
if [ -z "$repo_url" ]
|
199
|
+
then
|
200
|
+
die 7 "No remote found"
|
201
|
+
fi
|
202
|
+
if [[ $repo_url == *github.com/* ]]
|
203
|
+
then
|
204
|
+
owner_and_repo=${repo_url#*github.com/}
|
205
|
+
else
|
206
|
+
owner_and_repo=${repo_url##*:}
|
207
|
+
fi
|
208
|
+
# Get rid of the trailing .git
|
209
|
+
service=$(basename "$owner_and_repo" .git)
|
210
|
+
owner=$(dirname "$owner_and_repo")
|
211
|
+
|
212
|
+
full_tag=$IMAGE_NAME:$tag-ruby$RUBY_VERSION-$UBUNTU_VERSION
|
213
|
+
# Pass any extra arguments to the build command ("$@" contains the rest of the arguments)
|
214
|
+
$runtime build --tag "$full_tag" "$@" \
|
215
|
+
--label org.opencontainers.image.created="$(date --utc --iso-8601=seconds)" \
|
216
|
+
--label org.opencontainers.image.description="Image for $service" \
|
217
|
+
--label org.opencontainers.image.licenses="$LICENSE" \
|
218
|
+
--label org.opencontainers.image.revision="$revision" \
|
219
|
+
--label org.opencontainers.image.url="$repo_url" \
|
220
|
+
--label org.opencontainers.image.title="$IMAGE_NAME" \
|
221
|
+
--label org.opencontainers.image.source="Generated by ruby-automation's build_image.sh ($USER@$HOSTNAME)" \
|
222
|
+
--label org.opencontainers.image.version="$full_tag" \
|
223
|
+
--label shortref="$shortref" \
|
224
|
+
--build-arg UBUNTU_VERSION="$UBUNTU_VERSION" \
|
225
|
+
--build-arg RUBY_VERSION="$RUBY_VERSION" \
|
226
|
+
-f "$containerfile" "$BUILD_CONTEXT" || die 8 "Failed to build image"
|
227
|
+
|
228
|
+
[ $push -eq 1 ] || exit 0
|
229
|
+
if ! $runtime login --get-login "$REGISTRY" >/dev/null 2>/dev/null
|
230
|
+
then
|
231
|
+
printf "Not logged in to '%s', trying to login\n" "$REGISTRY" >&2
|
232
|
+
[ -z "$REGISTRY_TOKEN" ] && die 9 "No REGISTRY_TOKEN (nor GITHUB_TOKEN) set, cannot login"
|
233
|
+
printf "%s" "$REGISTRY_TOKEN" | $runtime login -u "$REGISTRY_TOKEN" --password-stdin "$REGISTRY" || die 10 "Failed to login to $REGISTRY"
|
234
|
+
fi
|
235
|
+
|
236
|
+
# Split 1.2.3 into 1.2.3, 1.2, 1. We want to tag our image with all 3 of these
|
237
|
+
mapfile -t tags < <(echo "$tag" | awk -F'.' 'NF==3{print; print $1"."$2; print $1; next} NF==2{print; print $1; next} {print}')
|
238
|
+
for t in "${tags[@]}"
|
239
|
+
do
|
240
|
+
new_tag=$IMAGE_NAME:$t-ruby$RUBY_VERSION-$UBUNTU_VERSION
|
241
|
+
registry_image_name="$REGISTRY/$owner/$new_tag"
|
242
|
+
if [ "$runtime" = "podman" ]
|
243
|
+
then
|
244
|
+
if [ "$full_tag" != "$new_tag" ]
|
245
|
+
then
|
246
|
+
debug "Tagging %s as %s\n" "$full_tag" "$new_tag"
|
247
|
+
podman tag "$full_tag" "$new_tag" || die 11 "Failed to tag image $full_tag as $new_tag"
|
248
|
+
fi
|
249
|
+
podman push "$new_tag" "$registry_image_name" || die 12 "Failed to push image $new_tag to $registry_image_name"
|
250
|
+
else
|
251
|
+
debug "Tagging %s as %s\n" "$full_tag" "$registry_image_name"
|
252
|
+
docker tag "$full_tag" "$registry_image_name" || die 13 "Failed to tag image $full_tag as $registry_image_name"
|
253
|
+
docker push "$registry_image_name" || die 14 "Failed to push image $new_tag to $registry_image_name"
|
254
|
+
fi
|
255
|
+
done
|
256
|
+
|
257
|
+
# vim: set foldmethod=marker et ts=4 sts=4 sw=4 ft=bash :
|
data/ci/publish-gem.sh
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
if readlink -f . >/dev/null 2>&1 # {{{ makes readlink work on mac
|
4
|
+
then
|
5
|
+
readlink=readlink
|
6
|
+
else
|
7
|
+
if greadlink -f . >/dev/null 2>&1
|
8
|
+
then
|
9
|
+
readlink=greadlink
|
10
|
+
else
|
11
|
+
printf "You must install greadlink to use this (brew install coreutils)\n" >&2
|
12
|
+
fi
|
13
|
+
fi # }}}
|
14
|
+
|
15
|
+
# Set here to the full path to this script
|
16
|
+
me=${BASH_SOURCE[0]}
|
17
|
+
[ -L "$me" ] && me=$($readlink -f "$me")
|
18
|
+
here=$(cd "$(dirname "$me")" && pwd)
|
19
|
+
just_me=$(basename "$me")
|
20
|
+
|
21
|
+
: "${GEM_NAME:=sequel-pgt_outbox}"
|
22
|
+
: "${GIT_ORG:=rubyists}"
|
23
|
+
|
24
|
+
GEM_HOST=$1
|
25
|
+
: "${GEM_HOST:=rubygems}"
|
26
|
+
|
27
|
+
case "$GEM_HOST" in
|
28
|
+
rubygems)
|
29
|
+
gem_key='rubygems'
|
30
|
+
gem_host='https://rubygems.org'
|
31
|
+
;;
|
32
|
+
github)
|
33
|
+
gem_key='github'
|
34
|
+
gem_host="https://rubygems.pkg.github.com/$GIT_ORG"
|
35
|
+
# Replace the gem host in the gemspec, so it allows pushing to the GitHub package registry
|
36
|
+
sed --in-place=.bak -e "s|https://rubygems.org|https://rubygems.pkg.github.com/$GIT_ORG|" "$here/../$GEM_NAME".gemspec
|
37
|
+
# Restore the original gemspec after the script finishes
|
38
|
+
trap 'mv -v "$here/../$GEM_NAME".gemspec.bak "$here/../$GEM_NAME".gemspec' EXIT
|
39
|
+
;;
|
40
|
+
*)
|
41
|
+
printf 'Unknown GEM_HOST: %s\n' "$GEM_HOST" >&2
|
42
|
+
exit 1
|
43
|
+
;;
|
44
|
+
esac
|
45
|
+
|
46
|
+
# We only want this part running in CI, with no ~/.gem dir
|
47
|
+
# For local testing, you should have a ~/.gem/credentials file with
|
48
|
+
# the keys you need to push to rubygems or github
|
49
|
+
if [ ! -d ~/.gem ]
|
50
|
+
then
|
51
|
+
if [ -z "$GEM_TOKEN" ]
|
52
|
+
then
|
53
|
+
printf 'No GEM_TOKEN provided, cannot publish\n' >&2
|
54
|
+
exit 1
|
55
|
+
fi
|
56
|
+
mkdir -p ~/.gem
|
57
|
+
printf '%s\n:%s: %s\n' '---' "$gem_key" "$GEM_TOKEN" > ~/.gem/credentials
|
58
|
+
chmod 600 ~/.gem/credentials
|
59
|
+
fi
|
60
|
+
|
61
|
+
if [ -f "$here"/../.version.txt ]
|
62
|
+
then
|
63
|
+
version=$(<"$here"/../.version.txt)
|
64
|
+
else
|
65
|
+
version=$(git describe --tags --abbrev=0 | sed -e 's/^v//')
|
66
|
+
fi
|
67
|
+
|
68
|
+
gem="$(printf '%s-%s.gem' "$GEM_NAME" "$version")"
|
69
|
+
if [[ "${TRACE:-false}" == true || "${ACTIONS_STEP_DEBUG:-false}" == true ]]
|
70
|
+
then
|
71
|
+
printf "DEBUG: [%s] Building And Publishing %s to %s\n" "$just_me" "$gem" "$gem_host" >&2
|
72
|
+
fi
|
73
|
+
|
74
|
+
bundle exec gem build
|
75
|
+
bundle exec gem push -k "$gem_key" --host "$gem_host" "$gem"
|
76
|
+
|
77
|
+
# vim: set foldmethod=marker et ts=4 sts=4 sw=4 ft=bash :
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../pgt_outbox'
|
4
|
+
|
5
|
+
# The pgt_outbox extension adds support to the Database instance for
|
6
|
+
# implementing the transactional outbox pattern using triggers.
|
7
|
+
module Sequel
|
8
|
+
# Postgres namespace
|
9
|
+
module Postgres
|
10
|
+
# The PgtOutboxMethods module provides methods for creating outbox tables and triggers
|
11
|
+
module PgtOutboxMethods
|
12
|
+
class_eval(&Rubyists::PgtOutbox::DEFINITION)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Database.register_extension(:pgt_outbox, Postgres::PgtOutboxMethods)
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../pgt_outbox'
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
module Postgres
|
7
|
+
# Extends the Sequel::Database class with the PgtOutbox methods
|
8
|
+
module DatabaseMethods
|
9
|
+
class_eval(&Rubyists::PgtOutbox::DEFINITION)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../pgt_outbox'
|
4
|
+
|
5
|
+
module Rubyists
|
6
|
+
# Top-level module for the PgtOutbox gem
|
7
|
+
module PgtOutbox
|
8
|
+
# The Outbox Function
|
9
|
+
class Function
|
10
|
+
include PgtOutbox
|
11
|
+
DEFAULT_OPTS = { language: :plpgsql, returns: :trigger, replace: true }.freeze
|
12
|
+
|
13
|
+
attr_reader(*%i[outbox db opts])
|
14
|
+
|
15
|
+
def self.create!(outbox, opts: {})
|
16
|
+
new(outbox, opts: opts).create!
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(outbox, opts: {})
|
20
|
+
@outbox = outbox
|
21
|
+
@db = outbox.db
|
22
|
+
@opts = opts
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
@name = opts.fetch(:function_name, "pgt_outbox_#{mangled_table_name(db, outbox.name)}")
|
27
|
+
end
|
28
|
+
|
29
|
+
%i[data_after_column data_before_column event_prefix event_type_column quoted_name].each do |meth|
|
30
|
+
define_method(meth) { outbox.send(meth) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def create!
|
34
|
+
db.create_function(name, function_sql, function_opts)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def function_sql
|
39
|
+
<<~SQL
|
40
|
+
BEGIN
|
41
|
+
#{depth_guard_clause}
|
42
|
+
IF (TG_OP = 'INSERT') THEN
|
43
|
+
INSERT INTO #{quoted_name} ("#{event_type_column}", "#{data_after_column}") VALUES
|
44
|
+
('#{event_prefix}_created', to_jsonb(NEW));
|
45
|
+
RETURN NEW;
|
46
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
47
|
+
INSERT INTO #{quoted_name} ("#{event_type_column}", "#{data_before_column}", "#{data_after_column}") VALUES
|
48
|
+
('#{event_prefix}_updated', to_jsonb(OLD), to_jsonb(NEW));
|
49
|
+
RETURN NEW;
|
50
|
+
ELSIF (TG_OP = 'DELETE') THEN
|
51
|
+
INSERT INTO #{quoted_name} ("#{event_type_column}", "#{data_before_column}") VALUES
|
52
|
+
('#{event_prefix}_deleted', to_jsonb(OLD));
|
53
|
+
RETURN OLD;
|
54
|
+
END IF;
|
55
|
+
END;
|
56
|
+
SQL
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def function_opts
|
62
|
+
@function_opts ||= DEFAULT_OPTS.merge(opts.fetch(:function_opts, {}))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../pgt_outbox'
|
4
|
+
require_relative 'function'
|
5
|
+
|
6
|
+
module Rubyists
|
7
|
+
module PgtOutbox
|
8
|
+
# The Outbox Table
|
9
|
+
class Table # rubocop:disable Metrics/ClassLength
|
10
|
+
attr_reader(*%i[table opts db])
|
11
|
+
|
12
|
+
def self.create!(table, db, opts: {})
|
13
|
+
new(table, db, opts:).create!
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(table, db, opts: {})
|
17
|
+
@table = table
|
18
|
+
@opts = opts
|
19
|
+
@db = db
|
20
|
+
end
|
21
|
+
|
22
|
+
def create!
|
23
|
+
db.run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"' if uuid_primary_key?
|
24
|
+
create_table!
|
25
|
+
integer_columns!
|
26
|
+
completed_column!
|
27
|
+
timestamp_columns!
|
28
|
+
string_columns!
|
29
|
+
jsonb_columns!
|
30
|
+
indexes!
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
@name ||= opts.fetch(:outbox_table, "#{table}_outbox")
|
36
|
+
end
|
37
|
+
|
38
|
+
def timestamp_type
|
39
|
+
opts.fetch(:timestamp_type, :timestamptz)
|
40
|
+
end
|
41
|
+
|
42
|
+
def quoted_name
|
43
|
+
@quoted_name ||= db.send(:quote_schema_table, name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def event_prefix
|
47
|
+
@event_prefix ||= opts.fetch(:event_prefix, table)
|
48
|
+
end
|
49
|
+
|
50
|
+
def created_column
|
51
|
+
@created_column ||= opts.fetch(:created_column, :created)
|
52
|
+
end
|
53
|
+
|
54
|
+
def updated_column
|
55
|
+
@updated_column ||= opts.fetch(:updated_column, :updated)
|
56
|
+
end
|
57
|
+
|
58
|
+
def completed_column
|
59
|
+
@completed_column ||= opts.fetch(:completed_column, :completed)
|
60
|
+
end
|
61
|
+
|
62
|
+
def attempts_column
|
63
|
+
@attempts_column ||= opts.fetch(:attempts_column, :attempts)
|
64
|
+
end
|
65
|
+
|
66
|
+
def attempted_column
|
67
|
+
@attempted_column ||= opts.fetch(:attempted_column, :attempted)
|
68
|
+
end
|
69
|
+
|
70
|
+
def event_type_column
|
71
|
+
@event_type_column ||= opts.fetch(:event_type_column, :event_type)
|
72
|
+
end
|
73
|
+
|
74
|
+
def last_error_column
|
75
|
+
@last_error_column ||= opts.fetch(:last_error_column, :last_error)
|
76
|
+
end
|
77
|
+
|
78
|
+
def data_after_column
|
79
|
+
@data_after_column ||= opts.fetch(:data_after_column, :data_after)
|
80
|
+
end
|
81
|
+
|
82
|
+
def data_before_column
|
83
|
+
@data_before_column ||= opts.fetch(:data_before_column, :data_before)
|
84
|
+
end
|
85
|
+
|
86
|
+
def metadata_column
|
87
|
+
@metadata_column ||= opts.fetch(:metadata_column, :metadata)
|
88
|
+
end
|
89
|
+
|
90
|
+
def boolean_completed_column
|
91
|
+
@boolean_completed_column ||= opts.fetch(:boolean_completed_column, false)
|
92
|
+
end
|
93
|
+
|
94
|
+
def uuid_primary_key?
|
95
|
+
@uuid_primary_key ||= opts.fetch(:uuid_primary_key, false)
|
96
|
+
end
|
97
|
+
|
98
|
+
def uuid_function
|
99
|
+
@uuid_function ||= opts.fetch(:uuid_function, :uuid_generate_v4)
|
100
|
+
end
|
101
|
+
|
102
|
+
def function
|
103
|
+
@function ||= Function.create!(self, opts:)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def create_table!
|
109
|
+
uuid_primary_key = uuid_primary_key?
|
110
|
+
uuid_func = uuid_function
|
111
|
+
db.create_table(name) do
|
112
|
+
if uuid_primary_key
|
113
|
+
uuid :id, default: Sequel.function(uuid_func), primary_key: true
|
114
|
+
else
|
115
|
+
primary_key :id
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def completed_column!
|
121
|
+
if boolean_completed_column
|
122
|
+
db.add_column name, completed_column, FalseClass, null: false, default: false
|
123
|
+
else
|
124
|
+
db.add_column name, completed_column, timestamp_type
|
125
|
+
end
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
def integer_columns!
|
130
|
+
db.add_column name, attempts_column, Integer, null: false, default: 0
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
def timestamp_columns!
|
135
|
+
db.add_column name, created_column, timestamp_type
|
136
|
+
db.add_column name, updated_column, timestamp_type
|
137
|
+
db.add_column name, attempted_column, timestamp_type
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def string_columns!
|
142
|
+
db.add_column name, event_type_column, String, null: false
|
143
|
+
db.add_column name, last_error_column, String
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
def jsonb_columns!
|
148
|
+
db.add_column name, data_before_column, :jsonb
|
149
|
+
db.add_column name, data_after_column, :jsonb
|
150
|
+
db.add_column name, metadata_column, :jsonb
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
def indexes!
|
155
|
+
db.add_index name, Sequel.asc(created_column)
|
156
|
+
db.add_index name, Sequel.desc(attempted_column)
|
157
|
+
self
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../pgt_outbox'
|
4
|
+
|
5
|
+
module Rubyists
|
6
|
+
module PgtOutbox
|
7
|
+
# The Outbox Trigger
|
8
|
+
class Trigger
|
9
|
+
DEFAULT_OPTS = { after: true, each_row: true }.freeze
|
10
|
+
|
11
|
+
attr_reader(*%i[db table function events opts])
|
12
|
+
|
13
|
+
def self.create!(db, table, function, events: %i[insert update delete], opts: { when: nil })
|
14
|
+
new(db, table, function, events: events, opts: opts).create!
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(db, table, function, events: %i[insert update delete], opts: { when: nil })
|
18
|
+
@db = db
|
19
|
+
@table = table
|
20
|
+
@function = function
|
21
|
+
@events = events
|
22
|
+
@opts = opts
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
@name ||= opts.fetch(:trigger_name, function)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create!
|
30
|
+
db.create_trigger(table, name, function, events:, each_row:, after:, when: where)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def after
|
35
|
+
trigger_opts.fetch(:after)
|
36
|
+
end
|
37
|
+
|
38
|
+
def where
|
39
|
+
opts.fetch(:when, nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def each_row
|
43
|
+
trigger_opts.fetch(:each_row)
|
44
|
+
end
|
45
|
+
|
46
|
+
def trigger_opts
|
47
|
+
@trigger_opts ||= DEFAULT_OPTS.merge(opts.fetch(:trigger_opts, {}))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|