diff --git a/document-versioning/.gitignore b/document-versioning/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/document-versioning/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/document-versioning/go.mod b/document-versioning/go.mod index 0516b27..5989e77 100644 --- a/document-versioning/go.mod +++ b/document-versioning/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.8.0 github.com/oapi-codegen/runtime v1.1.2 + github.com/stretchr/testify v1.11.1 github.com/wI2L/jsondiff v0.7.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -18,7 +19,7 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -37,7 +38,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -50,11 +51,9 @@ require ( go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.24.0 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/document-versioning/go.sum b/document-versioning/go.sum index b8012fc..6aff3bf 100644 --- a/document-versioning/go.sum +++ b/document-versioning/go.sum @@ -4,38 +4,22 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -44,46 +28,32 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -97,14 +67,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -122,18 +86,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -144,51 +103,25 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= -github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -197,5 +130,3 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/document-versioning/internal/database/document_versions.sql.go b/document-versioning/internal/database/document_versions.sql.go index c4d04f5..9174840 100644 --- a/document-versioning/internal/database/document_versions.sql.go +++ b/document-versioning/internal/database/document_versions.sql.go @@ -103,7 +103,6 @@ type GetDocumentVersionParams struct { Version int32 `json:"version"` } -// TODO: Implement this query // Retrieve a specific version record for a document // Parameters: document_id (UUID), version (INT) // Returns: document_id, version, patch, inverted_patch, created_at @@ -127,7 +126,6 @@ WHERE document_id = $1 ORDER BY version ASC ` -// TODO: Implement this query // Retrieve all versions for a document, ordered by version ascending // This is needed to reconstruct the document at any version // Parameters: document_id (UUID) @@ -164,7 +162,6 @@ FROM document_versions WHERE document_id = $1 ` -// TODO: Implement this query // Get the latest version number for a document // Parameters: document_id (UUID) // Returns: the maximum version number @@ -188,7 +185,6 @@ type GetVersionHistoryRow struct { CreatedAt pgtype.Timestamp `json:"created_at"` } -// TODO: Implement this query // Retrieve version metadata (version number and created_at) for displaying history // Parameters: document_id (UUID) // Returns: version, created_at, ordered by version DESC (newest first) diff --git a/document-versioning/internal/handler/handler.go b/document-versioning/internal/handler/handler.go index 34500a3..ac4c8f3 100644 --- a/document-versioning/internal/handler/handler.go +++ b/document-versioning/internal/handler/handler.go @@ -33,7 +33,6 @@ func (h *Handler) HealthCheck(c *gin.Context) { } // CreateDocument creates a new document -// TODO: Implement this handler // 1. Parse the request body into api.CreateDocumentRequest // 2. Call h.store.Create() with the name and content // 3. Return the created document as api.DocumentResponse with status 201 @@ -47,16 +46,36 @@ func (h *Handler) CreateDocument(c *gin.Context) { return } - // TODO: Call the store to create the document - // doc, err := h.store.Create(c.Request.Context(), req.Name, req.Content) + if req.Name == "" { + c.JSON(http.StatusBadRequest, api.ErrorResponse{ + Error: "name is required", + }) + return + } + if req.Content == nil { + c.JSON(http.StatusBadRequest, api.ErrorResponse{ + Error: "content is required", + }) + return + } - c.JSON(http.StatusNotImplemented, api.ErrorResponse{ - Error: "not implemented - complete this handler", + doc, err := h.store.Create(c.Request.Context(), req.Name, req.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, api.ErrorResponse{ + Error: "failed to create document: " + err.Error(), + }) + return + } + + c.JSON(http.StatusCreated, api.DocumentResponse{ + Id: doc.ID, + Name: doc.Name, + Content: doc.Content, + CreatedAt: doc.CreatedAt, }) } // GetDocument retrieves a document, optionally at a specific version -// TODO: Implement this handler // 1. The document ID is already parsed by the generated code // 2. Check if a version query parameter was provided (params.Version) // 3. If version provided, call h.store.GetAtVersion() @@ -64,21 +83,34 @@ func (h *Handler) CreateDocument(c *gin.Context) { // 5. Return the document as api.DocumentResponse // 6. Handle errors (404 for not found) func (h *Handler) GetDocument(c *gin.Context, id openapi_types.UUID, params api.GetDocumentParams) { - // The ID is already parsed by the generated wrapper code - // Use id directly (it's a uuid.UUID under the hood) - - _ = id // Use this parsed ID - - // TODO: Implement document retrieval - // Check params.Version to see if a specific version was requested + var doc *store.Document + var err error + var version int + + if params.Version != nil { + doc, err = h.store.GetAtVersion(c.Request.Context(), id, *params.Version) + version = *params.Version + } else { + doc, err = h.store.GetCurrent(c.Request.Context(), id) + version = doc.CurrentVersion + } + if err != nil { + c.JSON(http.StatusNotFound, api.ErrorResponse{ + Error: "document not found: " + err.Error(), + }) + return + } - c.JSON(http.StatusNotImplemented, api.ErrorResponse{ - Error: "not implemented - complete this handler", + c.JSON(http.StatusOK, api.DocumentResponse{ + Id: doc.ID, + Name: doc.Name, + Content: doc.Content, + CreatedAt: doc.CreatedAt, + Version: version, }) } // UpdateDocument updates a document, creating a new version -// TODO: Implement this handler // 1. The document ID is already parsed by the generated code // 2. Parse the request body into api.UpdateDocumentRequest // 3. Call h.store.Update() with the ID and new content @@ -93,33 +125,53 @@ func (h *Handler) UpdateDocument(c *gin.Context, id openapi_types.UUID) { return } - _ = id // Use this parsed ID - - // TODO: Call the store to update the document + doc, err := h.store.Update(c.Request.Context(), id, req.Content) + if err != nil { + c.JSON(http.StatusNotFound, api.ErrorResponse{ + Error: "document not found: " + err.Error(), + }) + return + } - c.JSON(http.StatusNotImplemented, api.ErrorResponse{ - Error: "not implemented - complete this handler", + c.JSON(http.StatusOK, api.DocumentResponse{ + Id: doc.ID, + Name: doc.Name, + Content: doc.Content, + CreatedAt: doc.CreatedAt, + Version: doc.CurrentVersion, }) } // ListVersions returns the version history for a document -// TODO: Implement this handler // 1. The document ID is already parsed by the generated code // 2. Call h.store.ListVersions() // 3. Return the version list as api.VersionListResponse // 4. Handle errors (404 for not found) func (h *Handler) ListVersions(c *gin.Context, id openapi_types.UUID) { - _ = id // Use this parsed ID + count, storedVersions, err := h.store.ListVersions(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, api.ErrorResponse{ + Error: "document not found: " + err.Error(), + }) + return + } - // TODO: Call the store to list versions + var versions []api.VersionInfo + for _, v := range storedVersions { + versions = append(versions, api.VersionInfo{ + Version: v.Version, + CreatedAt: v.CreatedAt, + }) + } - c.JSON(http.StatusNotImplemented, api.ErrorResponse{ - Error: "not implemented - complete this handler", + c.JSON(http.StatusOK, api.VersionListResponse{ + CurrentVersion: count, + DocumentId: id, + Versions: versions, }) } // RevertDocument reverts a document to a specific version -// TODO: Implement this handler // 1. The document ID is already parsed by the generated code // 2. Parse the request body into api.RevertRequest // 3. Call h.store.Revert() with the ID and target version @@ -134,11 +186,19 @@ func (h *Handler) RevertDocument(c *gin.Context, id openapi_types.UUID) { return } - _ = id // Use this parsed ID - - // TODO: Call the store to revert the document + doc, err := h.store.Revert(c.Request.Context(), id, req.Version) + if err != nil { + c.JSON(http.StatusNotFound, api.ErrorResponse{ + Error: "document not found or version invalid: " + err.Error(), + }) + return + } - c.JSON(http.StatusNotImplemented, api.ErrorResponse{ - Error: "not implemented - complete this handler", + c.JSON(http.StatusOK, api.DocumentResponse{ + Id: doc.ID, + Name: doc.Name, + Content: doc.Content, + CreatedAt: doc.CreatedAt, + Version: doc.CurrentVersion, }) } diff --git a/document-versioning/internal/store/mock.go b/document-versioning/internal/store/mock.go new file mode 100644 index 0000000..a7fb6ce --- /dev/null +++ b/document-versioning/internal/store/mock.go @@ -0,0 +1,62 @@ +package store + +import ( + "context" + "errors" + + "document-versioning/internal/database" + + "github.com/jackc/pgx/v5/pgtype" +) + +// MockDatabaseQueries is a mock implementation of database.Queries for testing +type MockDatabaseQueries struct { + CreateDocumentFunc func(ctx context.Context, name string) (database.Document, error) + CreateDocumentVersionFunc func(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) + GetDocumentFunc func(ctx context.Context, id pgtype.UUID) (database.Document, error) + GetDocumentVersionsFunc func(ctx context.Context, documentID pgtype.UUID) ([]database.DocumentVersion, error) + UpdateDocumentVersionFunc func(ctx context.Context, arg database.UpdateDocumentVersionParams) error + GetVersionHistoryFunc func(ctx context.Context, documentID pgtype.UUID) ([]database.GetVersionHistoryRow, error) +} + +func (m *MockDatabaseQueries) CreateDocument(ctx context.Context, name string) (database.Document, error) { + if m.CreateDocumentFunc != nil { + return m.CreateDocumentFunc(ctx, name) + } + return database.Document{}, errors.New("not implemented") +} + +func (m *MockDatabaseQueries) CreateDocumentVersion(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) { + if m.CreateDocumentVersionFunc != nil { + return m.CreateDocumentVersionFunc(ctx, arg) + } + return database.DocumentVersion{}, errors.New("not implemented") +} + +func (m *MockDatabaseQueries) GetDocument(ctx context.Context, id pgtype.UUID) (database.Document, error) { + if m.GetDocumentFunc != nil { + return m.GetDocumentFunc(ctx, id) + } + return database.Document{}, errors.New("not implemented") +} + +func (m *MockDatabaseQueries) GetDocumentVersions(ctx context.Context, documentID pgtype.UUID) ([]database.DocumentVersion, error) { + if m.GetDocumentVersionsFunc != nil { + return m.GetDocumentVersionsFunc(ctx, documentID) + } + return nil, errors.New("not implemented") +} + +func (m *MockDatabaseQueries) UpdateDocumentVersion(ctx context.Context, arg database.UpdateDocumentVersionParams) error { + if m.UpdateDocumentVersionFunc != nil { + return m.UpdateDocumentVersionFunc(ctx, arg) + } + return errors.New("not implemented") +} + +func (m *MockDatabaseQueries) GetVersionHistory(ctx context.Context, documentID pgtype.UUID) ([]database.GetVersionHistoryRow, error) { + if m.GetVersionHistoryFunc != nil { + return m.GetVersionHistoryFunc(ctx, documentID) + } + return nil, errors.New("not implemented") +} diff --git a/document-versioning/internal/store/store.go b/document-versioning/internal/store/store.go index 941647e..e9699bf 100644 --- a/document-versioning/internal/store/store.go +++ b/document-versioning/internal/store/store.go @@ -4,13 +4,33 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "document-versioning/internal/database" + jsonpatch "github.com/evanphx/json-patch/v5" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/wI2L/jsondiff" ) +func convertUUID(source pgtype.UUID) (uuid.UUID, error) { + if !source.Valid { + return uuid.Nil, fmt.Errorf("invalid pgtype.UUID") + } + result, err := uuid.FromBytes(source.Bytes[:]) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid UUID: %w", err) + } + return result, nil +} + +func setUUID(dest *pgtype.UUID, value uuid.UUID) { + dest.Bytes = value + dest.Valid = true +} + // Document represents a document with its current content type Document struct { ID uuid.UUID @@ -26,20 +46,39 @@ type VersionInfo struct { CreatedAt time.Time } +// DocumentStore handles document storage and versioning +// Queries defines the database operations needed by DocumentStore +// This interface allows tests to provide mocks without depending on sqlc's struct. +type Queries interface { + CreateDocument(ctx context.Context, name string) (database.Document, error) + CreateDocumentVersion(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) + GetDocument(ctx context.Context, id pgtype.UUID) (database.Document, error) + GetDocumentVersions(ctx context.Context, documentID pgtype.UUID) ([]database.DocumentVersion, error) + UpdateDocumentVersion(ctx context.Context, arg database.UpdateDocumentVersionParams) error + GetVersionHistory(ctx context.Context, documentID pgtype.UUID) ([]database.GetVersionHistoryRow, error) +} + // DocumentStore handles document storage and versioning type DocumentStore struct { - queries *database.Queries + queries Queries + + // helpers that can be overridden in tests + getAtVersion func(context.Context, uuid.UUID, int) (*Document, error) + update func(context.Context, uuid.UUID, map[string]interface{}) (*Document, error) } // NewDocumentStore creates a new DocumentStore -func NewDocumentStore(queries *database.Queries) *DocumentStore { - return &DocumentStore{ +func NewDocumentStore(queries Queries) *DocumentStore { + s := &DocumentStore{ queries: queries, } + // default helper implementations + s.getAtVersion = s.GetAtVersion + s.update = s.Update + return s } // Create creates a new document with the given content as version 1 -// TODO: Implement this method // 1. Create the document record in the database // 2. Marshal the content to JSON // 3. Create a patch from empty {} to the content (this is version 1) @@ -58,49 +97,61 @@ func (s *DocumentStore) Create(ctx context.Context, name string, content map[str return nil, fmt.Errorf("failed to marshal content: %w", err) } - // TODO: Create the initial patch from {} to content - // Use createPatch() helper function below - // Store the patch using s.queries.CreateDocumentVersion() + // Store the initial doc version + patch, invertedPatch, err := createPatch([]byte("{}"), contentBytes) + if err != nil { + return nil, fmt.Errorf("failed to create patch: %w", err) + } + _, err = s.queries.CreateDocumentVersion(ctx, database.CreateDocumentVersionParams{ + DocumentID: doc.ID, + Version: 1, + Patch: patch, + InvertedPatch: invertedPatch, + }) + if err != nil { + return nil, fmt.Errorf("failed to create document version: %w", err) + } - _ = contentBytes // Use this + id, err := convertUUID(doc.ID) + if err != nil { + return nil, fmt.Errorf("failed to convert document ID: %w", err) + } return &Document{ - ID: doc.ID, + ID: id, Name: doc.Name, CurrentVersion: int(doc.CurrentVersion), Content: content, - CreatedAt: doc.CreatedAt, + CreatedAt: doc.CreatedAt.Time, }, nil } // GetCurrent retrieves the current version of a document -// TODO: Implement this method // 1. Get the document record to find the current version // 2. Call GetAtVersion with the current version func (s *DocumentStore) GetCurrent(ctx context.Context, id uuid.UUID) (*Document, error) { - // Get document metadata - doc, err := s.queries.GetDocument(ctx, id) + var docID pgtype.UUID + setUUID(&docID, id) + + doc, err := s.queries.GetDocument(ctx, docID) if err != nil { return nil, fmt.Errorf("document not found: %w", err) } - // TODO: Reconstruct the document at current version - // Call GetAtVersion(ctx, id, int(doc.CurrentVersion)) - - _ = doc // Use this - - return nil, fmt.Errorf("not implemented") + return s.GetAtVersion(ctx, id, int(doc.CurrentVersion)) } // GetAtVersion retrieves a document at a specific version -// TODO: Implement this method // 1. Get all patches up to and including the target version // 2. Start with an empty document {} // 3. Apply each patch in order (version 1, 2, ..., N) // 4. Return the reconstructed document func (s *DocumentStore) GetAtVersion(ctx context.Context, id uuid.UUID, version int) (*Document, error) { + var docID pgtype.UUID + setUUID(&docID, id) + // Get document metadata - doc, err := s.queries.GetDocument(ctx, id) + doc, err := s.queries.GetDocument(ctx, docID) if err != nil { return nil, fmt.Errorf("document not found: %w", err) } @@ -109,21 +160,47 @@ func (s *DocumentStore) GetAtVersion(ctx context.Context, id uuid.UUID, version return nil, fmt.Errorf("version %d not found", version) } - // TODO: Get all versions up to the requested version - // Use s.queries.GetDocumentVersions(ctx, id) - // Filter to only include versions <= requested version + versions, err := s.queries.GetDocumentVersions(ctx, docID) + if err != nil { + return nil, fmt.Errorf("failed to get document versions: %w", err) + } + + // sanity check - versions must be in order + for i, v := range versions { + if int(v.Version) != i+1 { + return nil, fmt.Errorf("invalid version history: expected version %d but got %d", i+1, v.Version) + } + } - // TODO: Reconstruct the document by applying patches - // Start with base := []byte(`{}`) - // For each version, apply the patch using applyPatch() helper + // Filter to only include versions <= requested version + versions = versions[:version] + + // Reconstruct the document by applying patches + content := []byte(`{}`) + for _, v := range versions { + var err error + content, err = applyPatch(content, v.Patch) + if err != nil { + return nil, fmt.Errorf("failed to apply patch for version %d: %w", v.Version, err) + } + } - _ = doc // Use this + var rawJson map[string]interface{} + err = json.Unmarshal(content, &rawJson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal content: %w", err) + } - return nil, fmt.Errorf("not implemented") + return &Document{ + ID: id, + Name: doc.Name, + CurrentVersion: version, + Content: rawJson, + CreatedAt: doc.CreatedAt.Time, + }, nil } // Update updates a document with new content, creating a new version -// TODO: Implement this method // 1. Get the current document content // 2. Compute the patch from current to new content // 3. Store the new version with the patch @@ -147,74 +224,130 @@ func (s *DocumentStore) Update(ctx context.Context, id uuid.UUID, content map[st return nil, fmt.Errorf("failed to marshal new content: %w", err) } - // TODO: Create patch from current to new - // Use createPatch(currentBytes, newBytes) + forwardPatch, invertedPatch, err := createPatch(currentBytes, newBytes) + if err != nil { + return nil, fmt.Errorf("failed to create patch: %w", err) + } - // TODO: Create new version record - // newVersion := currentDoc.CurrentVersion + 1 - // s.queries.CreateDocumentVersion(...) + if len(forwardPatch) == 0 { + // No changes, return current document + return currentDoc, nil + } - // TODO: Update document's current version - // s.queries.UpdateDocumentVersion(...) + newVersion := currentDoc.CurrentVersion + 1 + docID := pgtype.UUID{} + setUUID(&docID, id) + _, err = s.queries.CreateDocumentVersion(ctx, database.CreateDocumentVersionParams{ + DocumentID: docID, + Version: int32(newVersion), + Patch: forwardPatch, + InvertedPatch: invertedPatch, + }) + if err != nil { + return nil, fmt.Errorf("failed to create document version: %w", err) + } - _ = currentBytes // Use these - _ = newBytes + err = s.queries.UpdateDocumentVersion(ctx, database.UpdateDocumentVersionParams{ + ID: docID, + CurrentVersion: int32(newVersion), + }) + if err != nil { + return nil, fmt.Errorf("failed to update document version: %w", err) + } - return nil, fmt.Errorf("not implemented") + return &Document{ + ID: id, + Name: currentDoc.Name, + CurrentVersion: newVersion, + Content: content, + CreatedAt: currentDoc.CreatedAt, + }, nil } // ListVersions returns the version history for a document -// TODO: Implement this method // 1. Get all versions for the document // 2. Return version metadata (version number, created_at) func (s *DocumentStore) ListVersions(ctx context.Context, id uuid.UUID) (int, []VersionInfo, error) { + var docID pgtype.UUID + setUUID(&docID, id) + // Verify document exists - doc, err := s.queries.GetDocument(ctx, id) + _, err := s.queries.GetDocument(ctx, docID) if err != nil { return 0, nil, fmt.Errorf("document not found: %w", err) } - // TODO: Get version history using s.queries.GetVersionHistory(ctx, id) - // Convert to []VersionInfo + rows, err := s.queries.GetVersionHistory(ctx, docID) + if err != nil { + return 0, nil, fmt.Errorf("failed to get version history: %w", err) + } - _ = doc // Use this + var versions []VersionInfo + for _, v := range rows { + versions = append(versions, VersionInfo{ + Version: int(v.Version), + CreatedAt: v.CreatedAt.Time, + }) + } - return 0, nil, fmt.Errorf("not implemented") + return len(versions), versions, nil } // Revert reverts a document to a specific version by creating a new version // with the content from the target version -// TODO: Implement this method // 1. Get the document at the target version // 2. Create a new version with that content (like Update) func (s *DocumentStore) Revert(ctx context.Context, id uuid.UUID, targetVersion int) (*Document, error) { - // Get document at target version targetDoc, err := s.GetAtVersion(ctx, id, targetVersion) if err != nil { return nil, fmt.Errorf("failed to get target version: %w", err) } - // TODO: Update the document with the target content - // This creates a new version with the reverted content - // Use s.Update(ctx, id, targetDoc.Content) - - _ = targetDoc // Use this + doc, err := s.Update(ctx, id, targetDoc.Content) + if err != nil { + return nil, fmt.Errorf("failed to update document: %w", err) + } - return nil, fmt.Errorf("not implemented") + return doc, nil } // Helper functions for JSON patch operations +// jsondiff Patch.String() returns results like "patch1\npatch2\npatch3", but +// to store them in a jsonb column we need to convert it to a valid JSON array +// eg. ["patch1","patch2","patch3"] +func convertPatchToJsonArray(patch jsondiff.Patch) []byte { + lines := strings.Split(patch.String(), "\n") + return []byte(fmt.Sprintf("[%s]", strings.Join(lines, ","))) +} + // createPatch creates a JSON patch from base to target, returning both // the forward patch and inverted patch -func createPatch(base, target []byte) (patch []byte, invertedPatch []byte, err error) { - // Use jsondiff to create an invertible patch - - return patchBytes, invertedBytes, nil +func createPatch(base, target []byte) ([]byte, []byte, error) { + patch, err := jsondiff.CompareJSON(base, target, jsondiff.Invertible()) + if err != nil { + return nil, nil, fmt.Errorf("failed to create patch: %w", err) + } + if len(patch) == 0 { + // optimization: if there are no changes, return empty + return []byte(""), []byte(""), nil + } + invertedPatch, err := patch.Invert() + if err != nil { + return nil, nil, fmt.Errorf("failed to invert patch: %w", err) + } + return convertPatchToJsonArray(patch), convertPatchToJsonArray(invertedPatch), nil } // applyPatch applies a JSON patch to a document func applyPatch(doc []byte, patchBytes []byte) ([]byte, error) { - - return result, nil + patch, err := jsonpatch.DecodePatch(patchBytes) + if err != nil { + return nil, fmt.Errorf("failed to decode patch: %w", err) + } + modified, err := patch.Apply(doc) + if err != nil { + return nil, fmt.Errorf("failed to apply patch: %w", err) + } + return modified, nil } diff --git a/document-versioning/internal/store/store_test.go b/document-versioning/internal/store/store_test.go new file mode 100644 index 0000000..fefd0e4 --- /dev/null +++ b/document-versioning/internal/store/store_test.go @@ -0,0 +1,241 @@ +package store + +import ( + "context" + "encoding/json" + "errors" + "os" + "path" + "testing" + "time" + + "document-versioning/internal/database" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Helper functions + +func createPgUUID(u uuid.UUID) pgtype.UUID { + return pgtype.UUID{Bytes: u, Valid: true} +} + +func createPgTimestamp(t time.Time) pgtype.Timestamp { + return pgtype.Timestamp{Time: t, Valid: true} +} + +// Tests for Create method + +func TestCreate_Successful(t *testing.T) { + testID := uuid.New() + createdAt := time.Now() + testContent := map[string]interface{}{ + "title": "Test Document", + "text": "Some content", + } + + mock := &MockDatabaseQueries{ + CreateDocumentFunc: func(ctx context.Context, name string) (database.Document, error) { + assert.Equal(t, "My Document", name) + return database.Document{ + ID: createPgUUID(testID), + Name: name, + CurrentVersion: 1, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + CreateDocumentVersionFunc: func(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) { + // Verify the version was created with correct parameters + assert.Equal(t, int32(1), arg.Version) + assert.NotEmpty(t, arg.Patch, "patch should not be empty") + assert.NotEmpty(t, arg.InvertedPatch, "inverted patch should not be empty") + + // Verify that the patch is valid JSON + var patches []interface{} + err := json.Unmarshal(arg.Patch, &patches) + assert.NoError(t, err, "patch should be valid JSON") + assert.NotEmpty(t, patches, "patch should contain operations") + + // Verify inverted patch is also valid JSON + var invertedPatches []interface{} + err = json.Unmarshal(arg.InvertedPatch, &invertedPatches) + assert.NoError(t, err, "inverted patch should be valid JSON") + + return database.DocumentVersion{ + DocumentID: arg.DocumentID, + Version: arg.Version, + Patch: arg.Patch, + InvertedPatch: arg.InvertedPatch, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + } + + store := NewDocumentStore(mock) + doc, err := store.Create(context.Background(), "My Document", testContent) + + require.NoError(t, err) + assert.NotNil(t, doc) + assert.Equal(t, testID, doc.ID) + assert.Equal(t, "My Document", doc.Name) + assert.Equal(t, 1, doc.CurrentVersion) + assert.Equal(t, testContent, doc.Content) + assert.Equal(t, createdAt, doc.CreatedAt) +} + +func TestCreate_CreateDocument_FailsOnDatabaseError(t *testing.T) { + mock := &MockDatabaseQueries{ + CreateDocumentFunc: func(ctx context.Context, name string) (database.Document, error) { + return database.Document{}, errors.New("database connection failed") + }, + } + + store := NewDocumentStore(mock) + doc, err := store.Create(context.Background(), "My Document", map[string]interface{}{}) + + assert.Error(t, err) + assert.Nil(t, doc) + assert.Contains(t, err.Error(), "failed to create document") + assert.Contains(t, err.Error(), "database connection failed") +} + +func TestCreate_CreateDocumentVersion_FailsOnDatabaseError(t *testing.T) { + testID := uuid.New() + createdAt := time.Now() + testContent := map[string]interface{}{"title": "Test"} + + mock := &MockDatabaseQueries{ + CreateDocumentFunc: func(ctx context.Context, name string) (database.Document, error) { + return database.Document{ + ID: createPgUUID(testID), + Name: name, + CurrentVersion: 1, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + CreateDocumentVersionFunc: func(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) { + return database.DocumentVersion{}, errors.New("version storage failed") + }, + } + + store := NewDocumentStore(mock) + doc, err := store.Create(context.Background(), "My Document", testContent) + + assert.Error(t, err) + assert.Nil(t, doc) + assert.Contains(t, err.Error(), "failed to create document version") + assert.Contains(t, err.Error(), "version storage failed") +} + +func TestCreate_WithEmptyContent(t *testing.T) { + testID := uuid.New() + createdAt := time.Now() + testContent := map[string]interface{}{} + + mock := &MockDatabaseQueries{ + CreateDocumentFunc: func(ctx context.Context, name string) (database.Document, error) { + return database.Document{ + ID: createPgUUID(testID), + Name: name, + CurrentVersion: 1, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + CreateDocumentVersionFunc: func(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) { + return database.DocumentVersion{ + DocumentID: arg.DocumentID, + Version: arg.Version, + Patch: arg.Patch, + InvertedPatch: arg.InvertedPatch, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + } + + store := NewDocumentStore(mock) + doc, err := store.Create(context.Background(), "Empty Doc", testContent) + + require.NoError(t, err) + assert.NotNil(t, doc) + assert.Equal(t, testID, doc.ID) + assert.Equal(t, testContent, doc.Content) + assert.Equal(t, 1, doc.CurrentVersion) +} + +func TestCreate_WithSampleContent(t *testing.T) { + testID := uuid.New() + createdAt := time.Now() + + // Load sample document from JSON file + exe, err := os.Executable() + require.NoError(t, err, "failed to read sample_document.json") + exeDir := path.Dir(exe) + sampleData, err := os.ReadFile(path.Join(exeDir, "../../../testdata/sample_document.json")) + require.NoError(t, err, "failed to read sample_document.json") + + var testContent map[string]interface{} + err = json.Unmarshal(sampleData, &testContent) + require.NoError(t, err, "failed to unmarshal sample_document.json") + + mock := &MockDatabaseQueries{ + CreateDocumentFunc: func(ctx context.Context, name string) (database.Document, error) { + return database.Document{ + ID: createPgUUID(testID), + Name: name, + CurrentVersion: 1, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + CreateDocumentVersionFunc: func(ctx context.Context, arg database.CreateDocumentVersionParams) (database.DocumentVersion, error) { + // Verify the version was created with correct parameters + assert.Equal(t, int32(1), arg.Version) + assert.NotEmpty(t, arg.Patch, "patch should not be empty") + assert.NotEmpty(t, arg.InvertedPatch, "inverted patch should not be empty") + + // Verify that the patch is valid JSON + var patches []interface{} + err := json.Unmarshal(arg.Patch, &patches) + assert.NoError(t, err, "patch should be valid JSON") + assert.NotEmpty(t, patches, "patch should contain operations") + + // Verify inverted patch is also valid JSON + var invertedPatches []interface{} + err = json.Unmarshal(arg.InvertedPatch, &invertedPatches) + assert.NoError(t, err, "inverted patch should be valid JSON") + + return database.DocumentVersion{ + DocumentID: arg.DocumentID, + Version: arg.Version, + Patch: arg.Patch, + InvertedPatch: arg.InvertedPatch, + CreatedAt: createPgTimestamp(createdAt), + }, nil + }, + } + + store := NewDocumentStore(mock) + doc, err := store.Create(context.Background(), "Infrastructure Asset", testContent) + + require.NoError(t, err) + assert.NotNil(t, doc) + assert.Equal(t, testContent, doc.Content) + + // Verify the nested structure is preserved + assert.Equal(t, "Infrastructure Asset", doc.Content["title"]) + assert.Equal(t, "electrical_transformer", doc.Content["type"]) + assert.Equal(t, "operational", doc.Content["status"]) + + // Verify location structure + location := doc.Content["location"].(map[string]interface{}) + assert.Equal(t, 40.7128, location["latitude"]) + assert.Equal(t, -74.0060, location["longitude"]) + assert.Equal(t, "123 Main Street", location["address"]) + + // Verify tags array + tags := doc.Content["tags"].([]interface{}) + assert.Len(t, tags, 3) + assert.Equal(t, "critical", tags[0]) +} diff --git a/document-versioning/migrations/queries/document_versions.sql b/document-versioning/migrations/queries/document_versions.sql index ccaccaa..03af409 100644 --- a/document-versioning/migrations/queries/document_versions.sql +++ b/document-versioning/migrations/queries/document_versions.sql @@ -27,7 +27,6 @@ VALUES ($1, $2, $3, $4) RETURNING document_id, version, patch, inverted_patch, created_at; -- name: GetDocumentVersion :one --- TODO: Implement this query -- Retrieve a specific version record for a document -- Parameters: document_id (UUID), version (INT) -- Returns: document_id, version, patch, inverted_patch, created_at @@ -36,7 +35,6 @@ FROM document_versions WHERE document_id = $1 AND version = $2; -- name: GetDocumentVersions :many --- TODO: Implement this query -- Retrieve all versions for a document, ordered by version ascending -- This is needed to reconstruct the document at any version -- Parameters: document_id (UUID) @@ -47,7 +45,6 @@ WHERE document_id = $1 ORDER BY version ASC; -- name: GetVersionHistory :many --- TODO: Implement this query -- Retrieve version metadata (version number and created_at) for displaying history -- Parameters: document_id (UUID) -- Returns: version, created_at, ordered by version DESC (newest first) @@ -57,7 +54,6 @@ WHERE document_id = $1 ORDER BY version DESC; -- name: GetLatestVersion :one --- TODO: Implement this query -- Get the latest version number for a document -- Parameters: document_id (UUID) -- Returns: the maximum version number