diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..084f944 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,210 @@ +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/assets/js/app.js b/assets/js/app.js index df0cdd9..88dbfaa 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,6 +1,6 @@ // If you want to use Phoenix channels, run `mix help phx.gen.channel` // to get started and then uncomment the line below. -// import "./user_socket.js" +import "./user_socket.js" // You can include dependencies in two ways. // @@ -21,9 +21,14 @@ import "phoenix_html" import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" +import { TextEditor } from './hooks/textEditHook' + let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) + +let Hooks = {} +Hooks.TextEditor = TextEditor +let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}}) // Show progress bar on live navigation and form submits topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) diff --git a/assets/js/hooks/textEditHook.js b/assets/js/hooks/textEditHook.js new file mode 100644 index 0000000..305ca4f --- /dev/null +++ b/assets/js/hooks/textEditHook.js @@ -0,0 +1,43 @@ +// https://elixirforum.com/t/how-to-connect-quill-with-phoenix/46004 +import Quill from 'quill'; +import socket from '../user_socket'; + +export let TextEditor = { + mounted() { + const padId = this.el.dataset.padId; + this.clientId; + + this.quill = new Quill(this.el, { + theme: 'snow' + }); + + let channel = socket.channel(`pad:${padId}`, {}); + + channel.join() + .receive("ok", ({uuid, contents}) => { + this.clientId = uuid; + this.quill.setContents(contents); + }) + // TODO: Probably need to show an alert that they couldn't join + .receive("error", resp => { console.log("Unable to join", resp) }); + + channel.on("update", ({change, client_id}) => { + if(client_id === this.clientId) return; + let range = this.quill.getSelection(); + this.quill.updateContents(change); + range && this.quill.setSelection(range.index, range.length); + }) + + this.quill.on('text-change', (delta, oldDelta, source) => { + if (delta == oldDelta) return; + if (source == 'api') { + console.log("An API call triggered this change."); + } else if (source == 'user') { + channel.push('update', {change: delta, client_id: this.clientId}); + } + }); + }, + updated(){ + console.log('U'); + } +} \ No newline at end of file diff --git a/assets/js/user_socket.js b/assets/js/user_socket.js new file mode 100644 index 0000000..2f5b4bc --- /dev/null +++ b/assets/js/user_socket.js @@ -0,0 +1,6 @@ +import {Socket} from "phoenix" + +let socket = new Socket("/socket", {params: {token: window.userToken}}) +socket.connect() + +export default socket; diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..d41847a --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,365 @@ +{ + "name": "assets", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "quill": "^1.3.7", + "quill-delta": "^5.1.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/quill/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, + "node_modules/quill/node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..af26a45 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "quill": "^1.3.7", + "quill-delta": "^5.1.0" + } +} diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index a4fbf7c..b43766d 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -6,6 +6,7 @@ const fs = require("fs") const path = require("path") module.exports = { + darkMode: 'media', content: [ "./js/**/*.js", "../lib/poex_web.ex", diff --git a/lib/poex/accounts/user.ex b/lib/poex/accounts/user.ex index 4ed7236..2d517c4 100644 --- a/lib/poex/accounts/user.ex +++ b/lib/poex/accounts/user.ex @@ -1,4 +1,8 @@ defmodule Poex.Accounts.User do + @moduledoc """ + A user changeset for registration. + """ + use Ecto.Schema import Ecto.Changeset diff --git a/lib/poex/accounts/user_notifier.ex b/lib/poex/accounts/user_notifier.ex index 1464fcf..cc129d2 100644 --- a/lib/poex/accounts/user_notifier.ex +++ b/lib/poex/accounts/user_notifier.ex @@ -1,4 +1,7 @@ defmodule Poex.Accounts.UserNotifier do + @moduledoc """ + Handle emails via Swoosh + """ import Swoosh.Email alias Poex.Mailer diff --git a/lib/poex/accounts/user_token.ex b/lib/poex/accounts/user_token.ex index 8414ff6..ac99e1d 100644 --- a/lib/poex/accounts/user_token.ex +++ b/lib/poex/accounts/user_token.ex @@ -1,4 +1,9 @@ defmodule Poex.Accounts.UserToken do + @moduledoc """ + Generates a token that will be stored in a signed place, + such as session or cookie. As they are signed, those + tokens do not need to be hashed. + """ use Ecto.Schema import Ecto.Query alias Poex.Accounts.UserToken diff --git a/lib/poex/application.ex b/lib/poex/application.ex index a23d2fc..9e7f7d1 100644 --- a/lib/poex/application.ex +++ b/lib/poex/application.ex @@ -12,8 +12,10 @@ defmodule Poex.Application do Poex.Repo, {DNSCluster, query: Application.get_env(:poex, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Poex.PubSub}, + {Registry, keys: :unique, name: Poex.Pads.DocumentRegistry}, # Start the Finch HTTP client for sending emails {Finch, name: Poex.Finch}, + Poex.Pads.DocumentDynamicSupervisor, # Start a worker by calling: Poex.Worker.start_link(arg) # {Poex.Worker, arg}, # Start to serve requests, typically the last entry diff --git a/lib/poex/mailer.ex b/lib/poex/mailer.ex index 3b0f7a3..969a82d 100644 --- a/lib/poex/mailer.ex +++ b/lib/poex/mailer.ex @@ -1,3 +1,4 @@ defmodule Poex.Mailer do + @moduledoc false use Swoosh.Mailer, otp_app: :poex end diff --git a/lib/poex/pads.ex b/lib/poex/pads.ex new file mode 100644 index 0000000..6241f4d --- /dev/null +++ b/lib/poex/pads.ex @@ -0,0 +1,30 @@ +defmodule Poex.Pads do + @moduledoc """ + The Accounts context. + """ + + import Ecto.Query, warn: false + alias Poex.Repo + alias Poex.Pads.Document + + def get_pad_document(id), do: Repo.get(Document, id) + + def update_pad_document(%Document{} = document, attrs) do + document + |> Document.changeset(attrs) + |> Repo.update() + end + + def update_pad_document(id, attrs) do + Repo.get(Document, id) + |> case do + nil -> + {:error, :not_found} + + document -> + document + |> Document.changeset(attrs) + |> Repo.update() + end + end +end diff --git a/lib/poex/pads/document.ex b/lib/poex/pads/document.ex new file mode 100644 index 0000000..a3752dd --- /dev/null +++ b/lib/poex/pads/document.ex @@ -0,0 +1,24 @@ +defmodule Poex.Pads.Document do + @moduledoc """ + Schema for PadDocuments created by users + """ + + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, Ecto.UUID, autogenerate: true} + schema "pad_documents" do + field :title, :string + field :state, :map, default: %{ops: []} + has_many :operations, Poex.Pads.Operation + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(document, attrs) do + document + |> cast(attrs, [:title, :state]) + |> cast_assoc(:operations) + end +end diff --git a/lib/poex/pads/document_server.ex b/lib/poex/pads/document_server.ex new file mode 100644 index 0000000..a786aa7 --- /dev/null +++ b/lib/poex/pads/document_server.ex @@ -0,0 +1,112 @@ +defmodule Poex.Pads.DocumentServer do + use GenServer + alias Poex.Utils.DeltaUtils + + @initial_state %{ + # Number of changes made to the document so far + version: 0, + + # An up-to-date Delta with all changes applied, representing + # the current state of the document + contents: [], + + # The `inverted` versions of all changes performed on the + # document (useful for viewing history or undo the changes) + inverted_changes: [] + } + + # Public API + # ---------- + + def start_link(args), do: GenServer.start_link(__MODULE__, args, name: via_tuple(args.id)) + def stop(pid), do: GenServer.stop(pid) + + def update(id, %{"change" => change, "client_id" => client_id}), + do: GenServer.call(via_tuple(id), {:update, change, client_id}) + + def get_contents(id), do: GenServer.call(via_tuple(id), :get_contents) + def get_history(id), do: GenServer.call(via_tuple(id), :get_history) + def undo(id), do: GenServer.call(via_tuple(id), :undo) + + # GenServer Callbacks + # ------------------- + + # Initialize the document with the default state + @impl true + def init(args) do + id = args.id + Registry.register(Poex.Pads.DocumentRegistry, id, []) + initial_state = Map.put(@initial_state, :id, id) + {:ok, initial_state} + end + + # Apply a given change to the document, updating its contents + # and incrementing the version + # + # We also keep track of the inverted version of the change + # which is useful for performing undo or viewing history + @impl true + def handle_call({:update, change_map, client_id}, _from, state) do + change = DeltaUtils.convert_ops(change_map) + inverted = Delta.invert(change, state.contents) + + state = %{ + id: state.id, + version: state.version + 1, + contents: Delta.compose(state.contents, change), + inverted_changes: [inverted | state.inverted_changes] + } + + PoexWeb.Endpoint.broadcast("pad:#{state.id}", "update", %{ + change: change, + client_id: client_id + }) + + {:reply, state.contents, state} + end + + # Fetch the current contents of the document + @impl true + def handle_call(:get_contents, _from, state) do + {:reply, state.contents, state} + end + + # Revert the applied changes one by one to see how the + # document transformed over time + @impl true + def handle_call(:get_history, _from, state) do + current = {state.version, state.contents} + + history = + Enum.scan(state.inverted_changes, current, fn inverted, {version, contents} -> + contents = Delta.compose(contents, inverted) + {version - 1, contents} + end) + + {:reply, [current | history], state} + end + + # Don't undo when document is already empty + @impl true + def handle_call(:undo, _from, %{version: 0} = state) do + {:reply, state.contents, state} + end + + # Revert the last change, removing it from our stack and + # updating the contents + @impl true + def handle_call(:undo, _from, state) do + [last_change | changes] = state.inverted_changes + + state = %{ + id: state.id, + version: state.version - 1, + contents: Delta.compose(state.contents, last_change), + inverted_changes: changes + } + + {:reply, state.contents, state} + end + + defp via_tuple(id), do: {:via, Registry, {Poex.Pads.DocumentRegistry, id}} +end diff --git a/lib/poex/pads/document_supervisor.ex b/lib/poex/pads/document_supervisor.ex new file mode 100644 index 0000000..57bdb7d --- /dev/null +++ b/lib/poex/pads/document_supervisor.ex @@ -0,0 +1,17 @@ +defmodule Poex.Pads.DocumentDynamicSupervisor do + use DynamicSupervisor + alias Poex.Pads.DocumentServer + + def start_link(init_arg) do + DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + def start_document_supervisor(args) do + spec = {DocumentServer, args} + DynamicSupervisor.start_child(__MODULE__, spec) + end + + def init(_init_arg) do + DynamicSupervisor.init(strategy: :one_for_one) + end +end diff --git a/lib/poex/pads/operation.ex b/lib/poex/pads/operation.ex new file mode 100644 index 0000000..380dcc6 --- /dev/null +++ b/lib/poex/pads/operation.ex @@ -0,0 +1,22 @@ +defmodule Poex.Pads.Operation do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, Ecto.UUID, autogenerate: true} + schema "operations" do + field :type, :string + field :value, :map + field :attributes, :map + belongs_to :document, Poex.Pads.Document + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(operation, attrs) do + operation + |> cast(attrs, [:type, :value, :attributes]) + |> validate_required([:type, :value]) + |> assoc_constraint(:document) + end +end diff --git a/lib/poex_web/channels/pad_channel.ex b/lib/poex_web/channels/pad_channel.ex new file mode 100644 index 0000000..fef1fac --- /dev/null +++ b/lib/poex_web/channels/pad_channel.ex @@ -0,0 +1,28 @@ +defmodule PoexWeb.PadChannel do + use Phoenix.Channel + alias Poex.Pads.DocumentServer + + @moduledoc """ + Channel for users of pad documents + """ + + def join("pad:lobby", _message, socket) do + {:ok, socket} + end + + def join("pad:" <> document_id, _params, socket) do + contents = DocumentServer.get_contents(document_id) + + {:ok, %{uuid: socket.assigns.uuid, contents: contents}, + assign(socket, :document_id, document_id)} + end + + def handle_in("update", %{"change" => change, "client_id" => client_id}, socket) do + DocumentServer.update(socket.assigns.document_id, %{ + "change" => change, + "client_id" => client_id + }) + + {:noreply, socket} + end +end diff --git a/lib/poex_web/channels/user_socket.ex b/lib/poex_web/channels/user_socket.ex new file mode 100644 index 0000000..a87dab1 --- /dev/null +++ b/lib/poex_web/channels/user_socket.ex @@ -0,0 +1,52 @@ +defmodule PoexWeb.UserSocket do + alias Ecto.UUID + use Phoenix.Socket + + # A Socket handler + # + # It's possible to control the websocket connection and + # assign values that can be accessed by your channel topics. + + ## Channels + channel "pad:*", PoexWeb.PadChannel + # + # To create a channel file, use the mix task: + # + # mix phx.gen.channel Room + # + # See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html) + # for further details. + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error` or `{:error, term}`. To control the + # response the client receives in that case, [define an error handler in the + # websocket + # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration). + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + @impl true + def connect(_params, socket, _connect_info) do + uuid = UUID.generate() + {:ok, assign(socket, :uuid, uuid)} + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # Elixir.PoexWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(_socket), do: nil +end diff --git a/lib/poex_web/components/layouts.ex b/lib/poex_web/components/layouts.ex index 48b3a0e..d37853e 100644 --- a/lib/poex_web/components/layouts.ex +++ b/lib/poex_web/components/layouts.ex @@ -1,4 +1,5 @@ defmodule PoexWeb.Layouts do + @moduledoc false use PoexWeb, :html embed_templates "layouts/*" diff --git a/lib/poex_web/components/layouts/app.html.heex b/lib/poex_web/components/layouts/app.html.heex index e23bfc8..a9efe86 100644 --- a/lib/poex_web/components/layouts/app.html.heex +++ b/lib/poex_web/components/layouts/app.html.heex @@ -1,32 +1,4 @@ -
-
-
- - - -

- v<%= Application.spec(:phoenix, :vsn) %> -

-
- -
-
-
-
- <.flash_group flash={@flash} /> - <%= @inner_content %> -
+
+ <.flash_group flash={@flash} /> + <%= @inner_content %>
diff --git a/lib/poex_web/components/layouts/root.html.heex b/lib/poex_web/components/layouts/root.html.heex index b89a7f4..0ded62e 100644 --- a/lib/poex_web/components/layouts/root.html.heex +++ b/lib/poex_web/components/layouts/root.html.heex @@ -8,6 +8,7 @@ <%= assigns[:page_title] || "Poex" %> + diff --git a/lib/poex_web/controllers/page_html/home.html.heex b/lib/poex_web/controllers/page_html/home.html.heex index e9fc48d..a9430f3 100644 --- a/lib/poex_web/controllers/page_html/home.html.heex +++ b/lib/poex_web/controllers/page_html/home.html.heex @@ -1,222 +1,22 @@ <.flash_group flash={@flash} /> - -
-
- -

- Phoenix Framework - - v<%= Application.spec(:phoenix, :vsn) %> - -

-

- Peace of mind from prototype to production. -

-

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -

-
- diff --git a/lib/poex_web/endpoint.ex b/lib/poex_web/endpoint.ex index 35daf35..129bce3 100644 --- a/lib/poex_web/endpoint.ex +++ b/lib/poex_web/endpoint.ex @@ -13,6 +13,10 @@ defmodule PoexWeb.Endpoint do socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + socket "/socket", PoexWeb.UserSocket, + websocket: true, + longpoll: false + # Serve at "/" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest diff --git a/lib/poex_web/live/pad/pad_live.ex b/lib/poex_web/live/pad/pad_live.ex new file mode 100644 index 0000000..851a31f --- /dev/null +++ b/lib/poex_web/live/pad/pad_live.ex @@ -0,0 +1,35 @@ +defmodule PoexWeb.PadLive do + alias Poex.Pads + alias Poex.Pads.Document + alias Poex.Repo + alias Poex.Utils + + use PoexWeb, :live_view + + def mount(%{"id" => "new"}, _session, socket) do + # Create a new PadDocument + {:ok, new_document} = + Document.changeset(%Document{}, %{title: "Untitled"}) + |> Repo.insert() + + Poex.Pads.DocumentDynamicSupervisor.start_document_supervisor(new_document) + + # Redirect to the new document with its ID + {:ok, push_navigate(socket, to: ~p"/pad/#{new_document.id}", replace: true)} + end + + def mount(%{"id" => id}, _session, socket) do + %{ + id: id, + title: title, + state: state + } = Repo.get!(Document, id) + + # init editor and assigns with latest state from doc + {:ok, assign(socket, id: id, title: title, state: state |> Utils.atomize_keys())} + end + + def handle_info(:new_document, socket) do + {:noreply, push_navigate(socket, to: "/pad/new")} + end +end diff --git a/lib/poex_web/live/pad/pad_live.html.heex b/lib/poex_web/live/pad/pad_live.html.heex new file mode 100644 index 0000000..55c8d70 --- /dev/null +++ b/lib/poex_web/live/pad/pad_live.html.heex @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/lib/poex_web/router.ex b/lib/poex_web/router.ex index 0666776..1adf079 100644 --- a/lib/poex_web/router.ex +++ b/lib/poex_web/router.ex @@ -61,6 +61,16 @@ defmodule PoexWeb.Router do post "/users/log_in", UserSessionController, :create end + scope "/", PoexWeb do + pipe_through [:browser] + + live_session :pad, + on_mount: [{PoexWeb.UserAuth, :mount_current_user}] do + live "/pad/:id", PadLive + live "/pad/new", PadLive, :new_document + end + end + scope "/", PoexWeb do pipe_through [:browser, :require_authenticated_user] diff --git a/lib/poex_web/telemetry.ex b/lib/poex_web/telemetry.ex index 36ff8a7..c6d2c74 100644 --- a/lib/poex_web/telemetry.ex +++ b/lib/poex_web/telemetry.ex @@ -1,4 +1,5 @@ defmodule PoexWeb.Telemetry do + @moduledoc false use Supervisor import Telemetry.Metrics diff --git a/lib/poex_web/user_auth.ex b/lib/poex_web/user_auth.ex index d4bee18..737f8eb 100644 --- a/lib/poex_web/user_auth.ex +++ b/lib/poex_web/user_auth.ex @@ -1,4 +1,8 @@ defmodule PoexWeb.UserAuth do + @moduledoc """ + This module handles user sessions + """ + use PoexWeb, :verified_routes import Plug.Conn diff --git a/lib/poex_web/utils/delta_utils.ex b/lib/poex_web/utils/delta_utils.ex new file mode 100644 index 0000000..ba9016d --- /dev/null +++ b/lib/poex_web/utils/delta_utils.ex @@ -0,0 +1,19 @@ +defmodule Poex.Utils.DeltaUtils do + alias Delta.Op + + def convert_ops(map) do + ops = Map.get(map, "ops") || [] + + Enum.map(ops, fn op -> + type = Map.keys(op) |> List.first() + value = Map.get(op, type) + attrs = Map.get(op, "attributes") + + case type do + "insert" -> Op.insert(value, attrs) + "delete" -> Op.delete(value) + "retain" -> Op.retain(value, attrs) + end + end) + end +end diff --git a/lib/poex_web/utils/utils.ex b/lib/poex_web/utils/utils.ex new file mode 100644 index 0000000..4314c3d --- /dev/null +++ b/lib/poex_web/utils/utils.ex @@ -0,0 +1,18 @@ +defmodule Poex.Utils do + def atomize_keys(%{__struct__: _} = map), do: map + + def atomize_keys(map) when is_map(map), + do: + Map.new(map, fn {k, v} -> + { + atomize_key(k), + atomize_keys(v) + } + end) + + def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys(&1)) + def atomize_keys(map), do: map + + defp atomize_key(key) when is_bitstring(key), do: String.to_atom(key) + defp atomize_key(key), do: key +end diff --git a/mix.exs b/mix.exs index b6a93b2..1bb224a 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,9 @@ defmodule Poex.MixProject do {:gettext, "~> 0.20"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, - {:plug_cowboy, "~> 2.5"} + {:plug_cowboy, "~> 2.5"}, + {:delta, "~> 0.2.0"}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index de0b56b..492606e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,15 @@ %{ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "delta": {:hex, :delta, "0.2.0", "1e0454b103a70e3c8465666588eeb14fa099ae40b5c5eefab035a05609cd345f", [:mix], [], "hexpm", "5b091fb47c8a9307ff39c2fd807493369bd92e00dbc8a65bc4d3135170cadcc5"}, "dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, @@ -41,6 +44,7 @@ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "text_delta": {:hex, :text_delta, "1.2.0", "392dff17d7b99467afa8cc41435af30c1f642e8ab4df45a68755343a225eb177", [:mix], [], "hexpm", "3437bfb63cf626176baf80040a621f17ff568d17ae46540bf55bbb7cfa6b2438"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, } diff --git a/priv/repo/migrations/20231114025400_create_pad_documents.exs b/priv/repo/migrations/20231114025400_create_pad_documents.exs new file mode 100644 index 0000000..19fe4dc --- /dev/null +++ b/priv/repo/migrations/20231114025400_create_pad_documents.exs @@ -0,0 +1,13 @@ +defmodule Poex.Repo.Migrations.CreatePadDocuments do + use Ecto.Migration + + def change do + create table(:pad_documents, primary_key: false) do + add :id, :uuid, primary_key: true, null: false + add :title, :string + add :state, :map, default: "{}" + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20231124191833_create_operations.exs b/priv/repo/migrations/20231124191833_create_operations.exs new file mode 100644 index 0000000..deeacc6 --- /dev/null +++ b/priv/repo/migrations/20231124191833_create_operations.exs @@ -0,0 +1,17 @@ +defmodule Poex.Repo.Migrations.CreateOperations do + use Ecto.Migration + + def change do + create table(:operations, primary_key: false) do + add :id, :uuid, primary_key: true, null: false + add :type, :string + add :value, :map + add :attributes, :map + add :document_id, references(:pad_documents, type: :uuid) + + timestamps() + end + + create index(:operations, [:document_id]) + end +end