brut 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +24 -3
- data/.nvim.lua +1 -0
- data/Dockerfile.dx +12 -3
- data/Gemfile.lock +9 -7
- data/README.md +0 -7
- data/Rakefile +6 -4
- data/bin/dev +20 -0
- data/bin/docs +27 -0
- data/bin/setup +47 -1
- data/brut-css/.nvim.lua +1 -0
- data/brut-css/README.md +28 -0
- data/brut-css/bin/build +31 -0
- data/brut-css/bin/dev +1 -0
- data/brut-css/bin/docs +15 -0
- data/brut-css/bin/setup +5 -0
- data/brut-css/config/media-queries-all.css +15 -0
- data/brut-css/config/media-queries-minimal.css +5 -0
- data/brut-css/config/postcss.config.cjs +7 -0
- data/brut-css/config/pseudo-classes-all.css +9 -0
- data/brut-css/dx +1 -0
- data/brut-css/package-lock.json +3217 -0
- data/brut-css/package.json +36 -0
- data/brut-css/src/css/appearance.css +145 -0
- data/brut-css/src/css/border.css +522 -0
- data/brut-css/src/css/colors.css +3502 -0
- data/brut-css/src/css/dimensions.css +548 -0
- data/brut-css/src/css/flex.css +179 -0
- data/brut-css/src/css/index.css +13 -0
- data/brut-css/src/css/layout.css +120 -0
- data/brut-css/src/css/list.css +41 -0
- data/brut-css/src/css/positioning.css +354 -0
- data/brut-css/src/css/properties/colors.css +455 -0
- data/brut-css/src/css/properties/index.css +3 -0
- data/brut-css/src/css/properties/spacing.css +140 -0
- data/brut-css/src/css/properties/typography.css +224 -0
- data/brut-css/src/css/reset.css +107 -0
- data/brut-css/src/css/spacing.css +585 -0
- data/brut-css/src/css/typography.css +519 -0
- data/brut-css/src/css/utils.css +104 -0
- data/brut-css/src/docs/1_getting-started/1_overview.md +46 -0
- data/brut-css/src/docs/1_getting-started/2_installation.md +25 -0
- data/brut-css/src/docs/1_getting-started/3_core-concepts.md +75 -0
- data/brut-css/src/docs/1_getting-started/4_simple-example.md +132 -0
- data/brut-css/src/docs/1_getting-started/page.html.ejs +10 -0
- data/brut-css/src/docs/2_properties/page.html.ejs +71 -0
- data/brut-css/src/docs/3_classes/color-demo.html.ejs +31 -0
- data/brut-css/src/docs/3_classes/page.html.ejs +87 -0
- data/brut-css/src/docs/4_customization/1_design-system.md +36 -0
- data/brut-css/src/docs/4_customization/2_breakpoints.md +75 -0
- data/brut-css/src/docs/4_customization/3_pseudo-classes.md +74 -0
- data/brut-css/src/docs/4_customization/4_advanced-configuration.md +40 -0
- data/brut-css/src/docs/4_customization/page.html.ejs +10 -0
- data/brut-css/src/docs/docs.css +98 -0
- data/brut-css/src/docs/includes/body-and-header.html.ejs +30 -0
- data/brut-css/src/docs/includes/footer-and-rest.html.ejs +9 -0
- data/brut-css/src/docs/includes/head.html.ejs +5 -0
- data/brut-css/src/docs/includes/nav.html.ejs +10 -0
- data/brut-css/src/docs/index.html.ejs +32 -0
- data/brut-css/src/docs/prism-twilight.min.css +1 -0
- data/brut-css/src/js/Logger.js +71 -0
- data/brut-css/src/js/build.js +111 -0
- data/brut-css/src/js/cli/CLIArgError.js +7 -0
- data/brut-css/src/js/cli/Debug.js +27 -0
- data/brut-css/src/js/cli/DocsDir.js +16 -0
- data/brut-css/src/js/cli/DocsTemplateSourceDir.js +16 -0
- data/brut-css/src/js/cli/InputFile.js +31 -0
- data/brut-css/src/js/cli/MediaQueryConfigFile.js +10 -0
- data/brut-css/src/js/cli/OutputFile.js +22 -0
- data/brut-css/src/js/cli/ParsedArg.js +17 -0
- data/brut-css/src/js/cli/PathToBrutCSSRoot.js +19 -0
- data/brut-css/src/js/cli/PseudoClassConfigFile.js +11 -0
- data/brut-css/src/js/cli.js +108 -0
- data/brut-css/src/js/docGenerator.js +467 -0
- data/brut-css/src/js/mediaQueryConfigParser.js +98 -0
- data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +42 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +185 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +7 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +73 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +12 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +5 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +45 -0
- data/brut-css/src/js/pseudoClassConfigParser.js +145 -0
- data/brut-js/.projections.json +10 -0
- data/brut-js/README.md +118 -0
- data/brut-js/bin/build +10 -0
- data/brut-js/bin/ci +5 -0
- data/brut-js/bin/setup +5 -0
- data/brut-js/docs/README.md +8 -0
- data/brut-js/docs/jsdoc-plugins/customElementTag.js +8 -0
- data/brut-js/docs/jsdoc-theme/publish.js +692 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +25 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +2 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +28 -0
- data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +327 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +111 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +132 -0
- data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +10 -0
- data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +199 -0
- data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +143 -0
- data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +2 -0
- data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +13 -0
- data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +32 -0
- data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +108 -0
- data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +8 -0
- data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +7 -0
- data/brut-js/docs/jsdoc.config.json +23 -0
- data/brut-js/docs/package-lock.json +343 -0
- data/brut-js/docs/package.json +7 -0
- data/brut-js/package-lock.json +2171 -0
- data/brut-js/package.json +32 -0
- data/brut-js/specs/AjaxSubmit.spec.js +256 -0
- data/brut-js/specs/Autosubmit.spec.js +127 -0
- data/brut-js/specs/ConfirmSubmit.spec.js +193 -0
- data/brut-js/specs/ConstraintViolationMessage.spec.js +33 -0
- data/brut-js/specs/ConstraintViolationMessages.spec.js +29 -0
- data/brut-js/specs/CopyToClipboard.spec.js +35 -0
- data/brut-js/specs/Form.spec.js +181 -0
- data/brut-js/specs/I18nTranslation.spec.js +19 -0
- data/brut-js/specs/LocaleDetection.spec.js +22 -0
- data/brut-js/specs/Message.spec.js +15 -0
- data/brut-js/specs/SpecHelper.js +23 -0
- data/brut-js/specs/Tabs.spec.js +41 -0
- data/brut-js/specs/config/asset_metadata.json +7 -0
- data/brut-js/src/AjaxSubmit.js +384 -0
- data/brut-js/src/Autosubmit.js +63 -0
- data/brut-js/src/BaseCustomElement.js +261 -0
- data/brut-js/src/ConfirmSubmit.js +116 -0
- data/brut-js/src/ConfirmationDialog.js +143 -0
- data/brut-js/src/ConstraintViolationMessage.js +125 -0
- data/brut-js/src/ConstraintViolationMessages.js +98 -0
- data/brut-js/src/CopyToClipboard.js +96 -0
- data/brut-js/src/Form.js +151 -0
- data/brut-js/src/I18nTranslation.js +61 -0
- data/brut-js/src/LocaleDetection.js +117 -0
- data/brut-js/src/Logger.js +90 -0
- data/brut-js/src/Message.js +56 -0
- data/brut-js/src/RichString.js +113 -0
- data/brut-js/src/Tabs.js +168 -0
- data/brut-js/src/Tracing.js +247 -0
- data/brut-js/src/appForTestingOnly.js +15 -0
- data/brut-js/src/index.js +130 -0
- data/brut-js/src/testing/AssetMetadata.js +35 -0
- data/brut-js/src/testing/AssetMetadataLoader.js +25 -0
- data/brut-js/src/testing/CustomElementTest.js +235 -0
- data/brut-js/src/testing/DOMCreator.js +45 -0
- data/brut-js/src/testing/index.js +48 -0
- data/brutrb.com/.vitepress/config.mjs +106 -0
- data/brutrb.com/.vitepress/plugins/jsdocLinker.js +34 -0
- data/brutrb.com/.vitepress/plugins/rdocLinker.js +18 -0
- data/brutrb.com/.vitepress/theme/custom.css +7 -0
- data/brutrb.com/.vitepress/theme/index.js +18 -0
- data/brutrb.com/.vitepress/theme/style.css +149 -0
- data/brutrb.com/ai.md +68 -0
- data/brutrb.com/assets.md +138 -0
- data/brutrb.com/bin/build +5 -0
- data/brutrb.com/bin/deploy +7 -0
- data/brutrb.com/bin/dev +5 -0
- data/brutrb.com/bin/setup +5 -0
- data/brutrb.com/brut-js.md +117 -0
- data/brutrb.com/business-logic.md +55 -0
- data/brutrb.com/cli.md +278 -0
- data/brutrb.com/components.md +243 -0
- data/brutrb.com/configuration.md +257 -0
- data/brutrb.com/css.md +103 -0
- data/brutrb.com/custom-element-tests.md +149 -0
- data/brutrb.com/database-access.md +201 -0
- data/brutrb.com/database-schema.md +312 -0
- data/brutrb.com/deployment.md +66 -0
- data/brutrb.com/dev-environment.md +179 -0
- data/brutrb.com/doc-conventions.md +39 -0
- data/brutrb.com/end-to-end-tests.md +174 -0
- data/brutrb.com/flash-and-session.md +224 -0
- data/brutrb.com/forms.md +866 -0
- data/brutrb.com/getting-started.md +66 -0
- data/brutrb.com/handlers.md +153 -0
- data/brutrb.com/hooks.md +178 -0
- data/brutrb.com/i18n.md +188 -0
- data/brutrb.com/images/Makefile +10 -0
- data/brutrb.com/images/dev-env-overview.dot +54 -0
- data/brutrb.com/images/dev-env-overview.png +0 -0
- data/brutrb.com/images/dev-env-protocol.dot +37 -0
- data/brutrb.com/images/dev-env-protocol.png +0 -0
- data/brutrb.com/images/logo-300.png +0 -0
- data/brutrb.com/images/logo.png +0 -0
- data/brutrb.com/images/overview.graffle +0 -0
- data/brutrb.com/images/overview.png +0 -0
- data/brutrb.com/images/spa.dot +19 -0
- data/brutrb.com/images/spa.png +0 -0
- data/brutrb.com/images/workspace-protocol.dot +44 -0
- data/brutrb.com/images/workspace-protocol.png +0 -0
- data/brutrb.com/index.md +36 -0
- data/brutrb.com/instrumentation.md +183 -0
- data/brutrb.com/javascript.md +122 -0
- data/brutrb.com/jobs.md +14 -0
- data/brutrb.com/keyword-injection.md +237 -0
- data/brutrb.com/markdown-examples.md +85 -0
- data/brutrb.com/middleware.md +80 -0
- data/brutrb.com/not-released.md +5 -0
- data/brutrb.com/overview.md +404 -0
- data/brutrb.com/package-lock.json +2404 -0
- data/brutrb.com/package.json +11 -0
- data/brutrb.com/pages.md +378 -0
- data/brutrb.com/public/images/logo-300.png +0 -0
- data/brutrb.com/public/images/logo.png +0 -0
- data/brutrb.com/routes.md +215 -0
- data/brutrb.com/security.md +105 -0
- data/brutrb.com/seed-data.md +63 -0
- data/brutrb.com/space-time-continuum.md +85 -0
- data/brutrb.com/tutorial.md +3 -0
- data/brutrb.com/unit-tests.md +148 -0
- data/docker-compose.dx.yml +6 -3
- data/docs/404.html +21 -0
- data/docs/CNAME +1 -0
- data/docs/ai.html +24 -0
- data/docs/api/Brut/BackEnd/SeedData.html +493 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +214 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq.html +125 -0
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +414 -0
- data/docs/api/Brut/BackEnd/Validators.html +128 -0
- data/docs/api/Brut/BackEnd.html +132 -0
- data/docs/api/Brut/CLI/App.html +1576 -0
- data/docs/api/Brut/CLI/AppRunner.html +491 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +264 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +306 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +262 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +314 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +183 -0
- data/docs/api/Brut/CLI/Apps/DB/Create.html +365 -0
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +357 -0
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +335 -0
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +329 -0
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +347 -0
- data/docs/api/Brut/CLI/Apps/DB/Status.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB.html +183 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +512 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +398 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +374 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +410 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +262 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +480 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +450 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +380 -0
- data/docs/api/Brut/CLI/Apps/Scaffold.html +253 -0
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +464 -0
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +407 -0
- data/docs/api/Brut/CLI/Apps/Test/JS.html +262 -0
- data/docs/api/Brut/CLI/Apps/Test/Run.html +578 -0
- data/docs/api/Brut/CLI/Apps/Test.html +253 -0
- data/docs/api/Brut/CLI/Apps.html +125 -0
- data/docs/api/Brut/CLI/Command.html +2342 -0
- data/docs/api/Brut/CLI/Error.html +139 -0
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +664 -0
- data/docs/api/Brut/CLI/ExecutionResults.html +675 -0
- data/docs/api/Brut/CLI/Executor.html +430 -0
- data/docs/api/Brut/CLI/InvalidOption.html +245 -0
- data/docs/api/Brut/CLI/Options.html +753 -0
- data/docs/api/Brut/CLI/Output.html +699 -0
- data/docs/api/Brut/CLI/SystemExecError.html +451 -0
- data/docs/api/Brut/CLI.html +263 -0
- data/docs/api/Brut/FactoryBot.html +225 -0
- data/docs/api/Brut/Framework/App.html +1097 -0
- data/docs/api/Brut/Framework/Config.html +1045 -0
- data/docs/api/Brut/Framework/Container.html +1379 -0
- data/docs/api/Brut/Framework/Error.html +140 -0
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +144 -0
- data/docs/api/Brut/Framework/Errors/Bug.html +234 -0
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +257 -0
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +273 -0
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +471 -0
- data/docs/api/Brut/Framework/Errors/NotFound.html +308 -0
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +234 -0
- data/docs/api/Brut/Framework/Errors.html +328 -0
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +392 -0
- data/docs/api/Brut/Framework/MCP.html +861 -0
- data/docs/api/Brut/Framework/ProjectEnvironment.html +648 -0
- data/docs/api/Brut/Framework.html +129 -0
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +317 -0
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +326 -0
- data/docs/api/Brut/FrontEnd/Component.html +365 -0
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +470 -0
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +518 -0
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +317 -0
- data/docs/api/Brut/FrontEnd/Components/Input.html +195 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +339 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +660 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +417 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +918 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +651 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +125 -0
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +367 -0
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +336 -0
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +655 -0
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +352 -0
- data/docs/api/Brut/FrontEnd/Components.html +135 -0
- data/docs/api/Brut/FrontEnd/Download.html +467 -0
- data/docs/api/Brut/FrontEnd/Flash.html +1150 -0
- data/docs/api/Brut/FrontEnd/Form.html +1157 -0
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +634 -0
- data/docs/api/Brut/FrontEnd/Forms/Input.html +615 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +547 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1318 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +587 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +613 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +582 -0
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms.html +127 -0
- data/docs/api/Brut/FrontEnd/GenericResponse.html +377 -0
- data/docs/api/Brut/FrontEnd/Handler.html +442 -0
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +318 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +336 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +399 -0
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +354 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +151 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +315 -0
- data/docs/api/Brut/FrontEnd/Handlers.html +125 -0
- data/docs/api/Brut/FrontEnd/HandlingResults.html +339 -0
- data/docs/api/Brut/FrontEnd/HttpMethod.html +661 -0
- data/docs/api/Brut/FrontEnd/HttpStatus.html +496 -0
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +284 -0
- data/docs/api/Brut/FrontEnd/Layout.html +318 -0
- data/docs/api/Brut/FrontEnd/Middleware.html +135 -0
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +288 -0
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +292 -0
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +324 -0
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +372 -0
- data/docs/api/Brut/FrontEnd/Middlewares.html +125 -0
- data/docs/api/Brut/FrontEnd/Page.html +773 -0
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +797 -0
- data/docs/api/Brut/FrontEnd/Pages.html +125 -0
- data/docs/api/Brut/FrontEnd/RequestContext.html +1312 -0
- data/docs/api/Brut/FrontEnd/RouteHook.html +424 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +242 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +249 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +264 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +261 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +284 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +252 -0
- data/docs/api/Brut/FrontEnd/RouteHooks.html +115 -0
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +227 -0
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +305 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +324 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +319 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +327 -0
- data/docs/api/Brut/FrontEnd/Routing/Route.html +761 -0
- data/docs/api/Brut/FrontEnd/Routing.html +927 -0
- data/docs/api/Brut/FrontEnd/Session.html +1195 -0
- data/docs/api/Brut/FrontEnd.html +134 -0
- data/docs/api/Brut/I18n/BaseMethods.html +931 -0
- data/docs/api/Brut/I18n/ForBackEnd.html +302 -0
- data/docs/api/Brut/I18n/ForCLI.html +302 -0
- data/docs/api/Brut/I18n/ForHTML.html +296 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +316 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +930 -0
- data/docs/api/Brut/I18n.html +127 -0
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +435 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +286 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +302 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +864 -0
- data/docs/api/Brut/Instrumentation.html +126 -0
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +532 -0
- data/docs/api/Brut/SinatraHelpers.html +281 -0
- data/docs/api/Brut/SpecSupport/ClockSupport.html +383 -0
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +502 -0
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +503 -0
- data/docs/api/Brut/SpecSupport/E2eSupport.html +142 -0
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +403 -0
- data/docs/api/Brut/SpecSupport/FlashSupport.html +278 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +401 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +195 -0
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +160 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +553 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +439 -0
- data/docs/api/Brut/SpecSupport/Matchers.html +125 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +335 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +602 -0
- data/docs/api/Brut/SpecSupport/SessionSupport.html +196 -0
- data/docs/api/Brut/SpecSupport.html +129 -0
- data/docs/api/Brut.html +225 -0
- data/docs/api/Clock.html +603 -0
- data/docs/api/RichString.html +968 -0
- data/docs/api/SemanticLogger/Appender/Async.html +219 -0
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +115 -0
- data/docs/api/Sequel/Extensions/BrutMigrations.html +533 -0
- data/docs/api/Sequel/Extensions.html +117 -0
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +105 -0
- data/docs/api/Sequel/Plugins/CreatedAt.html +125 -0
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +207 -0
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +186 -0
- data/docs/api/Sequel/Plugins/ExternalId.html +218 -0
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +202 -0
- data/docs/api/Sequel/Plugins/FindBang.html +125 -0
- data/docs/api/Sequel/Plugins.html +117 -0
- data/docs/api/Sequel.html +117 -0
- data/docs/api/_index.html +1553 -0
- data/docs/api/class_list.html +54 -0
- data/docs/api/css/common.css +1 -0
- data/docs/api/css/full_list.css +58 -0
- data/docs/api/css/style.css +503 -0
- data/docs/api/file.README.html +127 -0
- data/docs/api/file_list.html +59 -0
- data/docs/api/frames.html +22 -0
- data/docs/api/index.html +127 -0
- data/docs/api/js/app.js +344 -0
- data/docs/api/js/full_list.js +242 -0
- data/docs/api/js/jquery.js +4 -0
- data/docs/api/method_list.html +3998 -0
- data/docs/api/top-level-namespace.html +112 -0
- data/docs/assets/ai.md.tZrjP9im.js +1 -0
- data/docs/assets/ai.md.tZrjP9im.lean.js +1 -0
- data/docs/assets/app.D_yaTITQ.js +1 -0
- data/docs/assets/assets.md.D3wunzLx.js +19 -0
- data/docs/assets/assets.md.D3wunzLx.lean.js +1 -0
- data/docs/assets/brut-js.md.o2DAO2s2.js +12 -0
- data/docs/assets/brut-js.md.o2DAO2s2.lean.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.lean.js +1 -0
- data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +1 -0
- data/docs/assets/chunks/VPLocalSearchBox.B2-ZzyTY.js +8 -0
- data/docs/assets/chunks/framework.1L-BeKqY.js +18 -0
- data/docs/assets/chunks/theme.CfGFVRvE.js +2 -0
- data/docs/assets/cli.md.RmeA2b0i.js +127 -0
- data/docs/assets/cli.md.RmeA2b0i.lean.js +1 -0
- data/docs/assets/components.md.eCttGlN-.js +104 -0
- data/docs/assets/components.md.eCttGlN-.lean.js +1 -0
- data/docs/assets/configuration.md.BRriU0cL.js +78 -0
- data/docs/assets/configuration.md.BRriU0cL.lean.js +1 -0
- data/docs/assets/css.md.DJgj2clw.js +21 -0
- data/docs/assets/css.md.DJgj2clw.lean.js +1 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.js +69 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.lean.js +1 -0
- data/docs/assets/database-access.md.C7l-Vuvb.js +63 -0
- data/docs/assets/database-access.md.C7l-Vuvb.lean.js +1 -0
- data/docs/assets/database-schema.md.BUjR0VS1.js +63 -0
- data/docs/assets/database-schema.md.BUjR0VS1.lean.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.lean.js +1 -0
- data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
- data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.js +11 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.lean.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.lean.js +1 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.js +26 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +1 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.js +93 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.lean.js +1 -0
- data/docs/assets/forms.md.CBTYQ_Cz.js +379 -0
- data/docs/assets/forms.md.CBTYQ_Cz.lean.js +1 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.js +2 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +1 -0
- data/docs/assets/handlers.md.089DVD3v.js +69 -0
- data/docs/assets/handlers.md.089DVD3v.lean.js +1 -0
- data/docs/assets/hooks.md.C4-moMny.js +80 -0
- data/docs/assets/hooks.md.C4-moMny.lean.js +1 -0
- data/docs/assets/i18n.md.Do9i1qWl.js +23 -0
- data/docs/assets/i18n.md.Do9i1qWl.lean.js +1 -0
- data/docs/assets/index.md.B28EwVpq.js +1 -0
- data/docs/assets/index.md.B28EwVpq.lean.js +1 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.js +35 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.lean.js +1 -0
- data/docs/assets/javascript.md.GWbhRS51.js +31 -0
- data/docs/assets/javascript.md.GWbhRS51.lean.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.lean.js +1 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.js +25 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.lean.js +1 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.js +33 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +1 -0
- data/docs/assets/middleware.md.Czz_UlJN.js +20 -0
- data/docs/assets/middleware.md.Czz_UlJN.lean.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.lean.js +1 -0
- data/docs/assets/overview.Da81cB9R.png +0 -0
- data/docs/assets/overview.md.CDalkuxV.js +133 -0
- data/docs/assets/overview.md.CDalkuxV.lean.js +1 -0
- data/docs/assets/pages.md.BE3kfOc5.js +122 -0
- data/docs/assets/pages.md.BE3kfOc5.lean.js +1 -0
- data/docs/assets/routes.md.BMM7peut.js +29 -0
- data/docs/assets/routes.md.BMM7peut.lean.js +1 -0
- data/docs/assets/security.md.C668yXCi.js +1 -0
- data/docs/assets/security.md.C668yXCi.lean.js +1 -0
- data/docs/assets/seed-data.md.BvFZlqIk.js +14 -0
- data/docs/assets/seed-data.md.BvFZlqIk.lean.js +1 -0
- data/docs/assets/spa.qejUdp-5.png +0 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.js +1 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.lean.js +1 -0
- data/docs/assets/style.D73IYGCX.css +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.js +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.lean.js +1 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.js +13 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +1 -0
- data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
- data/docs/assets.html +42 -0
- data/docs/brut-css/brut.css +1 -0
- data/docs/brut-css/brut.max.css +22372 -0
- data/docs/brut-css/classes/appearances.html +783 -0
- data/docs/brut-css/classes/background-colors.html +3529 -0
- data/docs/brut-css/classes/border-colors.html +3529 -0
- data/docs/brut-css/classes/borders.html +2293 -0
- data/docs/brut-css/classes/dimensions.html +2581 -0
- data/docs/brut-css/classes/flex.html +917 -0
- data/docs/brut-css/classes/foreground-colors.html +3261 -0
- data/docs/brut-css/classes/junk-drawer.html +431 -0
- data/docs/brut-css/classes/layout.html +668 -0
- data/docs/brut-css/classes/lists.html +331 -0
- data/docs/brut-css/classes/positioning.html +1751 -0
- data/docs/brut-css/classes/spacings.html +2633 -0
- data/docs/brut-css/classes/typography.html +2206 -0
- data/docs/brut-css/customization/advanced-configuration.html +204 -0
- data/docs/brut-css/customization/breakpoints.html +227 -0
- data/docs/brut-css/customization/design-system.html +197 -0
- data/docs/brut-css/customization/pseudo-classes.html +228 -0
- data/docs/brut-css/docs.css +98 -0
- data/docs/brut-css/getting-started/core-concepts.html +234 -0
- data/docs/brut-css/getting-started/installation.html +190 -0
- data/docs/brut-css/getting-started/overview.html +210 -0
- data/docs/brut-css/getting-started/simple-example.html +285 -0
- data/docs/brut-css/index.html +193 -0
- data/docs/brut-css/prism-twilight.min.css +1 -0
- data/docs/brut-css/properties/colors.html +1548 -0
- data/docs/brut-css/properties/spacings.html +614 -0
- data/docs/brut-css/properties/typography.html +777 -0
- data/docs/brut-js/api/AjaxSubmit.html +374 -0
- data/docs/brut-js/api/AjaxSubmit.js.html +435 -0
- data/docs/brut-js/api/Autosubmit.html +192 -0
- data/docs/brut-js/api/Autosubmit.js.html +114 -0
- data/docs/brut-js/api/BaseCustomElement.html +1091 -0
- data/docs/brut-js/api/BaseCustomElement.js.html +312 -0
- data/docs/brut-js/api/BrutCustomElements.html +172 -0
- data/docs/brut-js/api/BufferedLogger.html +173 -0
- data/docs/brut-js/api/ConfirmSubmit.html +278 -0
- data/docs/brut-js/api/ConfirmSubmit.js.html +167 -0
- data/docs/brut-js/api/ConfirmationDialog.html +425 -0
- data/docs/brut-js/api/ConfirmationDialog.js.html +194 -0
- data/docs/brut-js/api/ConstraintViolationMessage.html +448 -0
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +176 -0
- data/docs/brut-js/api/ConstraintViolationMessages.html +590 -0
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +149 -0
- data/docs/brut-js/api/CopyToClipboard.html +345 -0
- data/docs/brut-js/api/CopyToClipboard.js.html +147 -0
- data/docs/brut-js/api/Form.html +294 -0
- data/docs/brut-js/api/Form.js.html +202 -0
- data/docs/brut-js/api/I18nTranslation.html +409 -0
- data/docs/brut-js/api/I18nTranslation.js.html +112 -0
- data/docs/brut-js/api/LocaleDetection.html +312 -0
- data/docs/brut-js/api/LocaleDetection.js.html +168 -0
- data/docs/brut-js/api/Logger.html +702 -0
- data/docs/brut-js/api/Logger.js.html +141 -0
- data/docs/brut-js/api/Message.html +238 -0
- data/docs/brut-js/api/Message.js.html +107 -0
- data/docs/brut-js/api/PrefixedLogger.html +369 -0
- data/docs/brut-js/api/RichString.html +1049 -0
- data/docs/brut-js/api/RichString.js.html +164 -0
- data/docs/brut-js/api/Tabs.html +295 -0
- data/docs/brut-js/api/Tabs.js.html +219 -0
- data/docs/brut-js/api/Tracing.html +277 -0
- data/docs/brut-js/api/Tracing.js.html +298 -0
- data/docs/brut-js/api/external-CustomElementRegistry.html +140 -0
- data/docs/brut-js/api/external-Performance.html +138 -0
- data/docs/brut-js/api/external-Promise.html +138 -0
- data/docs/brut-js/api/external-ValidityState.html +138 -0
- data/docs/brut-js/api/external-Window.html +233 -0
- data/docs/brut-js/api/external-fetch.html +138 -0
- data/docs/brut-js/api/global.html +400 -0
- data/docs/brut-js/api/index.html +168 -0
- data/docs/brut-js/api/index.js.html +181 -0
- data/docs/brut-js/api/module-testing.html +383 -0
- data/docs/brut-js/api/scripts/linenumber.js +25 -0
- data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/docs/brut-js/api/scripts/prettify/lang-css.js +2 -0
- data/docs/brut-js/api/scripts/prettify/prettify.js +28 -0
- data/docs/brut-js/api/styles/jsdoc-default.css +327 -0
- data/docs/brut-js/api/styles/prettify-jsdoc.css +111 -0
- data/docs/brut-js/api/styles/prettify-tomorrow.css +132 -0
- data/docs/brut-js/api/testing.AssetMetadata.html +172 -0
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +171 -0
- data/docs/brut-js/api/testing.CustomElementTest.html +679 -0
- data/docs/brut-js/api/testing.DOMCreator.html +171 -0
- data/docs/brut-js/api/testing_AssetMetadata.js.html +86 -0
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +76 -0
- data/docs/brut-js/api/testing_CustomElementTest.js.html +286 -0
- data/docs/brut-js/api/testing_DOMCreator.js.html +96 -0
- data/docs/brut-js/api/testing_index.js.html +99 -0
- data/docs/brut-js.html +35 -0
- data/docs/business-logic.html +24 -0
- data/docs/cli.html +150 -0
- data/docs/components.html +127 -0
- data/docs/configuration.html +101 -0
- data/docs/css.html +44 -0
- data/docs/custom-element-tests.html +92 -0
- data/docs/database-access.html +86 -0
- data/docs/database-schema.html +86 -0
- data/docs/deployment.html +24 -0
- data/docs/dev-environment.html +34 -0
- data/docs/doc-conventions.html +24 -0
- data/docs/end-to-end-tests.html +49 -0
- data/docs/flash-and-session.html +116 -0
- data/docs/forms.html +402 -0
- data/docs/getting-started.html +25 -0
- data/docs/handlers.html +92 -0
- data/docs/hashmap.json +1 -0
- data/docs/hooks.html +103 -0
- data/docs/i18n.html +46 -0
- data/docs/images/logo-300.png +0 -0
- data/docs/images/logo.png +0 -0
- data/docs/index.html +24 -0
- data/docs/instrumentation.html +58 -0
- data/docs/javascript.html +54 -0
- data/docs/jobs.html +24 -0
- data/docs/keyword-injection.html +48 -0
- data/docs/markdown-examples.html +56 -0
- data/docs/middleware.html +43 -0
- data/docs/not-released.html +24 -0
- data/docs/overview.html +156 -0
- data/docs/pages.html +145 -0
- data/docs/routes.html +52 -0
- data/docs/security.html +24 -0
- data/docs/seed-data.html +37 -0
- data/docs/space-time-continuum.html +24 -0
- data/docs/tutorial.html +24 -0
- data/docs/unit-tests.html +36 -0
- data/docs/vp-icons.css +1 -0
- data/lib/brut/cli/app_runner.rb +1 -1
- data/lib/brut/cli/apps/test.rb +5 -0
- data/lib/brut/framework/mcp.rb +0 -4
- data/lib/brut/front_end/component.rb +59 -84
- data/lib/brut/front_end/components/constraint_violations.rb +1 -4
- data/lib/brut/front_end/components/form_tag.rb +1 -1
- data/lib/brut/front_end/components/input.rb +3 -3
- data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
- data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +6 -8
- data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
- data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
- data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
- data/lib/brut/front_end/components/time_tag.rb +2 -1
- data/lib/brut/front_end/form.rb +4 -4
- data/lib/brut/front_end/forms/input.rb +2 -1
- data/lib/brut/front_end/forms/input_definition.rb +5 -2
- data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
- data/lib/brut/front_end/forms/select_input.rb +2 -4
- data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
- data/lib/brut/front_end/handler.rb +28 -26
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
- data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
- data/lib/brut/front_end/page.rb +4 -6
- data/lib/brut/front_end/request_context.rb +3 -2
- data/lib/brut/i18n/base_methods.rb +136 -82
- data/lib/brut/i18n/for_back_end.rb +1 -0
- data/lib/brut/i18n/for_cli.rb +1 -0
- data/lib/brut/i18n/for_html.rb +32 -4
- data/lib/brut/instrumentation/open_telemetry.rb +12 -2
- data/lib/brut/sinatra_helpers.rb +10 -3
- data/lib/brut/spec_support/component_support.rb +18 -18
- data/lib/brut/spec_support/e2e_test_server.rb +3 -0
- data/lib/brut/version.rb +1 -1
- data/lib/sequel/extensions/brut_migrations.rb +12 -9
- metadata +647 -5
- data/lib/brut/front_end/components/inputs/select.rb +0 -117
@@ -0,0 +1,201 @@
|
|
1
|
+
# Database Access / Data Models
|
2
|
+
|
3
|
+
Brut provides access to the database via the [Sequel library](https://sequel.jeremyevans.net/). Sequel is fully featured and provides a lot of ways of interacting with and managing your database. Brut includes several plugins and extensions to provide opinionated default behavior or additional features.
|
4
|
+
|
5
|
+
One thing to keep in mind is that Brut refers to your database layer as *database models* (notably not the un-qualified "models"). Brut treats this layer as a *model* of your database, not a model of your *domain* (though you are free to conflate the two).
|
6
|
+
|
7
|
+
This section details how to access data in your database.
|
8
|
+
|
9
|
+
> [!NOTE]
|
10
|
+
> Brut currently only supports Postgres. Sequel supports many database systems, however Brut's extensions are
|
11
|
+
> currently geared toward Postgres only.
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
Accessing your database in Brut uses Sequel's `Sequel::Model`. A base class called `AppDataModel` exists in your
|
16
|
+
app from which all other data models extend:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# app/src/back_end/data_models/app_data_model.rb
|
20
|
+
AppDataModel = Class.new(Sequel::Model)
|
21
|
+
class AppDataModel
|
22
|
+
# You can insert your own shared methods here
|
23
|
+
end
|
24
|
+
|
25
|
+
# app/src/back_end/data_models/db/account.rb
|
26
|
+
class DB::Account < AppDataModel
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
All data models are in the `DB` namespace. This clearly identifies a model as a model of your database and
|
31
|
+
not your domain.
|
32
|
+
|
33
|
+
Inside a data model, you can use all of Sequel's API. In particular, you will want to use its API for
|
34
|
+
[associations](https://sequel.jeremyevans.net/rdoc/files/doc/association_basics_rdoc.html) so that you can create
|
35
|
+
relationships between models.
|
36
|
+
|
37
|
+
In your business logic or front-end code, you can access your data using these models:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
account = DB::Account.find(email: form.email)
|
41
|
+
```
|
42
|
+
|
43
|
+
> [!IMPORTANT]
|
44
|
+
> Sequel's `Sequel::Model` is different from Active Record, especially when it comes to associations.
|
45
|
+
> `account.organizations` would return an `Array` of `DB::Organization` records, all fetched from the database.
|
46
|
+
> `account.organizations_dataset` would return a active-relation style object to allow stacking
|
47
|
+
> quieries. **Please** familiarize yourself with Sequel's API.
|
48
|
+
|
49
|
+
## Testing
|
50
|
+
|
51
|
+
Testing, as it applies to data models, is made up of two parts: managing test *data* and testing the models
|
52
|
+
themselves.
|
53
|
+
|
54
|
+
### Test Data is Managed with FactoryBot
|
55
|
+
|
56
|
+
Brut apps come with [FactoryBot](https://github.com/thoughtbot/factory_bot) installed, and this is how you should
|
57
|
+
create test (and seed) data.
|
58
|
+
|
59
|
+
Factories for data models live in `specs/factories/db`. Because data models are in the `DB` namespace, you will
|
60
|
+
need to explicitly state the `class:` in the factory, but otherwise, you can use FactoryBot in a conventional
|
61
|
+
way. [Faker](https://github.com/faker-ruby/faker) is also installed to allow you to create realistic and
|
62
|
+
randomized data.
|
63
|
+
|
64
|
+
Here is a factory for our hypothetical account:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# specs/factories/db/account.factory.rb
|
68
|
+
FactoryBot.define do
|
69
|
+
factory :account, class: "DB::Account" do
|
70
|
+
email { Faker::Internet.unique.email }
|
71
|
+
organization
|
72
|
+
|
73
|
+
trait :inactive do
|
74
|
+
deactivated_at { Time.now }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
The `spec_support.rb` file generated when you created your Brut app should ensure that `FactoryBot::Syntax::Methods` is included in all specs, meaning you can do `create(:account)` to create an instance of `DB::Account`.
|
81
|
+
|
82
|
+
See [Unit Tests](/unit-tests) for more details on testing and Factory Bot setup.
|
83
|
+
|
84
|
+
### Testing Your Data Models
|
85
|
+
|
86
|
+
In general, you don't want to test the configuration in your data models. For example, testing that
|
87
|
+
`account.organization = organization` works is largely pointless, since this is provided by Sequel.
|
88
|
+
|
89
|
+
That said, if you have complex or unusual database constraints, having a test for them can be valuable.
|
90
|
+
|
91
|
+
Suppose our `DB::Account` has the following check constraint that requires an email end with `@example.com`:
|
92
|
+
|
93
|
+
```ruby {13-16}
|
94
|
+
Sequel.migration do
|
95
|
+
up do
|
96
|
+
create_table :accounts,
|
97
|
+
comment: "People or systems who can access this system",
|
98
|
+
external_id: true do
|
99
|
+
|
100
|
+
column :email, :text, unique: true
|
101
|
+
foreign_key :organization_id, :organizations
|
102
|
+
column :deactivated_at, :timestamptz, null: true
|
103
|
+
|
104
|
+
key [:email, :organization_id]
|
105
|
+
|
106
|
+
constraint(
|
107
|
+
:email_must_be_domain,
|
108
|
+
"email ~* '@example.com$'"
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
To test this, you would try to write invalid data into the database an ensure the expected exception is raised:
|
116
|
+
|
117
|
+
```ruby {11}
|
118
|
+
# specs/back_end/data_models/db/account.spec.rb
|
119
|
+
require "spec_helper"
|
120
|
+
RSpec.describe DB::Account do
|
121
|
+
describe "email" do
|
122
|
+
it "must end in @example.com" do
|
123
|
+
expect {
|
124
|
+
DB::Account.create(
|
125
|
+
email: "pat@example.net",
|
126
|
+
organization: create(:organization)
|
127
|
+
)
|
128
|
+
}.to raise_error(Sequel::CheckConstraintViolation)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
If you don't want to be overly coupled to Sequel's exceptions, you can also assert on the message Postgres will
|
135
|
+
produce, which would include the name of the violated constraint:
|
136
|
+
|
137
|
+
```ruby {11}
|
138
|
+
# specs/back_end/data_models/db/account.spec.rb
|
139
|
+
require "spec_helper"
|
140
|
+
RSpec.describe DB::Account do
|
141
|
+
describe "email" do
|
142
|
+
it "must end in @example.com" do
|
143
|
+
expect {
|
144
|
+
DB::Account.create(
|
145
|
+
email: "pat@example.net",
|
146
|
+
organization: create(:organization)
|
147
|
+
)
|
148
|
+
}.to raise_error(/email_must_be_domain/)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
## Recommended Practices
|
155
|
+
|
156
|
+
### Do Not Put Business Logic On Your Database Models
|
157
|
+
|
158
|
+
There's no reason to, or benefit to doing so. What you'll find is that any app of even moderate complexity will
|
159
|
+
not have a strict mapping from page to business concept to database table. Rather these things will all differ
|
160
|
+
greatly, and each serves a different purpose.
|
161
|
+
|
162
|
+
The job of your data models—and the tables they provide access to—is to store reliable and unambiguous data.
|
163
|
+
Their job is to ensure there is no bad data such that when you ask the database a question, you get a reliable
|
164
|
+
and correct answer.
|
165
|
+
|
166
|
+
Your views and business logic do not have this exact same job.
|
167
|
+
|
168
|
+
As such, your models should only contain:
|
169
|
+
|
170
|
+
* configuration to allow navigating the database.
|
171
|
+
* methods to manage type conversions between your types and the strings or numbers required in the database
|
172
|
+
* methods to query the data based on data definitions (not business logic).
|
173
|
+
|
174
|
+
Business logic and data models *do* overlap at times, so there is some judgement in maintaining a clear
|
175
|
+
separation of concerns. One way to manage this is to always put all logic elsewhere until you see a pattern of
|
176
|
+
re-use that leads you to extract that logic to a data model.
|
177
|
+
|
178
|
+
### Do Not Use Validations on Models Unless There is No Other Choice
|
179
|
+
|
180
|
+
Sequel provides a validation layer for use on models. You should not generally use this, since a) data integrity
|
181
|
+
is baked into your database design, and b) user interactions and constraints are part of the front-end.
|
182
|
+
|
183
|
+
That said, there are times when you have data constraints that cannot be modeled in the database. In that case,
|
184
|
+
a validation on the data model is better than nothing. Since all data access for your app should go through your
|
185
|
+
data models, a validation on a data model has a high chance of being checked.
|
186
|
+
|
187
|
+
> [!NOTE]
|
188
|
+
> Since any process, app, or tool can manipulate your database, model-based validations won't be
|
189
|
+
> in effect, and therefore won't be applied. This is why you design your schema to avoid invalid
|
190
|
+
> data wherever possible.
|
191
|
+
|
192
|
+
|
193
|
+
## Technical Notes
|
194
|
+
|
195
|
+
> [!IMPORTANT]
|
196
|
+
> Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
|
197
|
+
> internals, the source code is always more correct.
|
198
|
+
|
199
|
+
_Last Updated May 8, 2025_
|
200
|
+
|
201
|
+
None at this time
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# Database Schema / Migrations
|
2
|
+
|
3
|
+
Brut provides access to the database via the [Sequel library](https://sequel.jeremyevans.net/). Sequel is fully featured and provides a lot of ways of interacting with and managing your database. Brut includes several plugins and extensions to provide opinionated default behavior or additional features.
|
4
|
+
|
5
|
+
One thing to keep in mind is that Brut refers to your database layer as *database models* (notably not the un-qualified "models"). Brut treats this layer as a *model* of your database, not a model of your *domain* (though you are free to conflate the two).
|
6
|
+
|
7
|
+
This section details how to manage your database schema.
|
8
|
+
|
9
|
+
> [!NOTE]
|
10
|
+
> Brut currently only supports Postgres. Sequel supports many database systems, however Brut's extensions are
|
11
|
+
> currently geared toward Postgres only.
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
Brut uses *migrations* to control and manage the schema of your database. Migrations are changes to the
|
16
|
+
schema that depend on the changes before them. In a running production database, you will not be able to
|
17
|
+
create the database schema from scratch—you will have to modify the existing schema to produce the schema
|
18
|
+
you want.
|
19
|
+
|
20
|
+
For example, if you have a table `widgets` that has a `name` and `description`, to add a `status` field,
|
21
|
+
you cannot `drop table widgets` and then `create table widgets(...)` with the fields. You must instead
|
22
|
+
`alter table widgets(...)` to add the new column.
|
23
|
+
|
24
|
+
Thus, each migration file is a change to the schema produced by all previous migration files.
|
25
|
+
|
26
|
+
Brut's provides this via Sequels. See [both](https://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html) [docs](https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html) for details on the API. Any schema modification method Sequel documents is available, however some default behavior has changed.
|
27
|
+
|
28
|
+
Schema files are located in `app/src/back_end/data_models/migrations` and are named using a timestamp-based
|
29
|
+
scheme. This means that when you create a new migration, its name will be based on the time and date you
|
30
|
+
created it, and any migrations that have not been applied will be applied in timestamp order.
|
31
|
+
|
32
|
+
### Creating Migrations
|
33
|
+
|
34
|
+
To create a migration, use `bin/db new-migration`. It accepts any number of arguments that will be joined
|
35
|
+
together to form the filename:
|
36
|
+
|
37
|
+
```
|
38
|
+
> bin/db new-migration user accounts
|
39
|
+
[ bin/db ] Migration created:
|
40
|
+
app/src/back_end/data_models/migrations/20250508132646_user-accounts.rb
|
41
|
+
```
|
42
|
+
|
43
|
+
The file is created mostly blank:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Sequel.migration do
|
47
|
+
up do
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Sequels' migration system is similar to Active Record's/Rails' in design and spirit, but the API is different.
|
53
|
+
Please consult the documentation and don't assume Active Record's DSL will work. It will not.
|
54
|
+
|
55
|
+
One thing to note is that Brut encourages the creation of only "up" migrations. That is, migrations that change a
|
56
|
+
database. "Down" migrations, which revert a change, are discouraged. See *Recommended Practices* for a detailed
|
57
|
+
explanation.
|
58
|
+
|
59
|
+
This is also why Sequel's `change` method is not included in the scaffolded code. `change`, like Active Record's
|
60
|
+
method of the same name, automagically creates both "up" and "down" migrations, but *only* if you use the DSL. If
|
61
|
+
you use raw SQL, `change` doesn't work. But that doesn't matter for Brut (again, see *Recommended Practices*).
|
62
|
+
|
63
|
+
Let's create a user accounts table that has an email field, a `deactivated_at` timestamp, and a `created_at`
|
64
|
+
timestamp:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Sequel.migration do
|
68
|
+
up do
|
69
|
+
create_table :accounts,
|
70
|
+
comment: "People or systems who can access this system" do
|
71
|
+
|
72
|
+
column :email, :text, unique: true
|
73
|
+
column :deactivated_at, :timestamptz, null: true
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
A few notes that aren't obvious without knowing about Brut's extensions:
|
81
|
+
|
82
|
+
* `comment:` is required. You must provide documentation about what table is for
|
83
|
+
* The table has a primary key named `id` of type `int` that is a serial.
|
84
|
+
* `created_at` is created by default, with time `timestamptz` (AKA `timestamp with time zone`, see [Space/Time Continuum](/space-time-continuum)).
|
85
|
+
* `email` is not null by default. `deactivated_at` *is* null because it's specified as such.
|
86
|
+
|
87
|
+
To apply this migration use `bin/db migrate`
|
88
|
+
|
89
|
+
```
|
90
|
+
> bin/db migrate
|
91
|
+
```
|
92
|
+
|
93
|
+
If you create a new migration, it will use a timestamp that is alphanumerically greater than the one we just made
|
94
|
+
and thus that migration will be applied after this one. Thus, you can rely on previous migrations having been
|
95
|
+
applied when authoring new ones.
|
96
|
+
|
97
|
+
### Managing Migrations
|
98
|
+
|
99
|
+
Sequel uses a special database table to understand which migrations have been run. This table will exist in
|
100
|
+
production and prevent you from applying migrations twice or skipping a migration.
|
101
|
+
|
102
|
+
Note that managing a production database in this way requires knowledge of both your database system and the data
|
103
|
+
itself. Brut can only provide so much to make this process manageable. You should consult [Strong Migrations'
|
104
|
+
README](https://github.com/ankane/strong_migrations?tab=readme-ov-file) and learn it deeply. Although it's
|
105
|
+
targeted at Rails developers, the information here applies to any database management system.
|
106
|
+
|
107
|
+
### Brut Extensions and Changes in Sequel's Behavior
|
108
|
+
|
109
|
+
Brut includes the following standard plugins and extensions:
|
110
|
+
|
111
|
+
* [`pg_array`](https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/pg_array_rb.html)
|
112
|
+
* [`pg_json`](https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/pg_json_rb.html)
|
113
|
+
* [`table_select`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TableSelect.html), which
|
114
|
+
changes queries to prepend `*` with the table name, e.g. `select accounts.*` instead of `select *`.
|
115
|
+
* [`skip_saving_columns`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/SkipSavingColumns.html) which will skip saving columns that the database generates.
|
116
|
+
|
117
|
+
Brut also provides the following plugins and behavior changes:
|
118
|
+
|
119
|
+
* `Sequel::Extensions::BrutInstrumentation`, which adds OpenTelemetry instrumentation to Sequel (see [Instrumentation](/instrumentation)).
|
120
|
+
* `Sequel::Plugins::FindBang`, which adds `find!` to all models. This wraps Sequel's `first!` method, but
|
121
|
+
provides a more helpful error message when no records are found
|
122
|
+
* `Sequel::Plugins::CreatedAt`, which automatically sets `created_at` when a record is created.
|
123
|
+
* `Sequel::Plugins::ExternalId`, which adds support for external IDs (see below)
|
124
|
+
* `Sequel::Extensions::BrutMigrations`, which enhances the migrations API (see below)
|
125
|
+
|
126
|
+
#### External IDs
|
127
|
+
|
128
|
+
It's often useful to provide a unique identifier for a record that is not the database primary key. There are
|
129
|
+
many advantages to doing so, but the core value Brut has regarding this is that database primary and foreign keys
|
130
|
+
are considered private and internal and for developer use only. It is trivial to produce externalizable keys, as
|
131
|
+
you'll see, so there's no reason to expose primary keys.
|
132
|
+
|
133
|
+
> [!NOTE]
|
134
|
+
> Take care to differentiate the terms *primary key* and *key*. In relational database literature, and thus
|
135
|
+
> in Brut, a *key* is any value that uniquely identifies a record. `email` in the `accounts` table above
|
136
|
+
> is a key. The *primary key* is single source of truth for identifying records internally in the database.
|
137
|
+
> Thus, it can be used as a *foreign key* to create relationships between tables. Brut further implements
|
138
|
+
> this as a *surrogate* or *synthetic* key, which means the value itself has no business or domain meaning.
|
139
|
+
|
140
|
+
In Brut, an external ID is automatically generated by the database when a record is created. By convention, it
|
141
|
+
is prefixed with a short string representing your app and a short string representing the table, followed by a
|
142
|
+
unique hash.
|
143
|
+
|
144
|
+
For example, if our app's prefix is, say, "my" (for "my app"), and the accounts table's prefix is "ac" (for "accounts"), an external ID might look like `myac_3457238947239487`. This double-prefixing is extremely useful when sharing these values with the outside world. You can immediately identify an ID from your app *and* know what sort of thing it refers to.
|
145
|
+
|
146
|
+
To use external IDs in Brut, you must do three things:
|
147
|
+
|
148
|
+
1. You must set your external ID prefix in `app/src/app.rb`. This should have been done when you created your
|
149
|
+
Brut app, but it looks like so:
|
150
|
+
|
151
|
+
```ruby {5}
|
152
|
+
class App < Brut::App
|
153
|
+
# ...
|
154
|
+
def initialize
|
155
|
+
# ...
|
156
|
+
Brut.container.override("external_id_prefix","my")
|
157
|
+
end
|
158
|
+
|
159
|
+
# ...
|
160
|
+
end
|
161
|
+
```
|
162
|
+
2. When creating the table in a migration, use `external_id: true`:
|
163
|
+
```ruby {5}
|
164
|
+
Sequel.migration do
|
165
|
+
up do
|
166
|
+
create_table :accounts,
|
167
|
+
comment: "People or systems who can access this system",
|
168
|
+
external_id: true do
|
169
|
+
|
170
|
+
column :email, :text, unique: true
|
171
|
+
column :deactivated_at, :timestamptz, null: true
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
3. In your [data model class](/database-access), use `has_external_id` to specify the prefix for this table:
|
178
|
+
|
179
|
+
```
|
180
|
+
class DB::Account < AppDataModel
|
181
|
+
has_external_id :ac
|
182
|
+
|
183
|
+
# ...
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
Brut creates the external ID using Ruby code as part of Sequel's lifecycle hooks. It's only set a) on creation, and b) if there is no value provided when creating the record.
|
188
|
+
|
189
|
+
This means that you can set values explicitly if you like, *and* you can change them later. This is useful if you shared the value with someone you didn't mean to. Because these external IDs aren't use for referential integrity/foreign keys, they can be changed at any time, as long as the value is unique (which will be enforced by the database).
|
190
|
+
|
191
|
+
### Brut Migration Changes and Enhancement
|
192
|
+
|
193
|
+
Brut attempts to set default behavior for migrations to encourage a modicum of best practices. This lists out
|
194
|
+
the changes and a brief explanation for the purpose of the change.
|
195
|
+
|
196
|
+
* **Automatic synthetic primary key named `id` of type `int`.** You almost always want this. You can change the
|
197
|
+
primary key configuration per table if you like, but if you do nothing, you get a primary key that works for 99%
|
198
|
+
of your needs.
|
199
|
+
* **Automatic `created_at` of type `timestamptz`.** It's a good practice to store the date a record was created. This can help with debugging and provide a reliable sort key for data that otherwise has none. It uses `timestamp with time zone`, which you are encouraged to use always. See [Space/Time Continuum](/space-time-continuum) for details.
|
200
|
+
* **No automatic `updated_at`.** While you are free to add `updated_at`, in practice this column creates more problems than it solves. If you need to know when data has changed, it is almost always better to do this with an audit table, event log, or special-purpose field.
|
201
|
+
* **`create_table` requires `comment:`.** Just document your tables. It takes two seconds and can save a lot of
|
202
|
+
time later.
|
203
|
+
* **Support for external IDs via `external_id:`.** As discussed above, this will create a unique `external_id`
|
204
|
+
column on your table and ensure it has a value on creation.
|
205
|
+
* **Columns are `NOT NULL` by default.** Null is not a valid value. In many cases, your columns should not allow
|
206
|
+
`NULL` (`nil`), so in Brut apps, you must opt into nullable columns. You can use `null: true` to make a column
|
207
|
+
nullable.
|
208
|
+
* **Foreign keys are `NOT NULL` and have an index created for them by default.** Foreign keys should rarely be
|
209
|
+
`NULL` and you almost always want an index on them, since you are likely to using them in queries, e.g.
|
210
|
+
`account.widgets` would join on `accounts.widget_id`. You can opt out of either via `null: true` and `index:
|
211
|
+
false`.
|
212
|
+
* **The method `key` allows you to specify a non-primary key, AKA a unique index**. Suppose our `accounts` table
|
213
|
+
allowed duplicate email addresses, but only one per `organization_id`. You'd model this by creating a unique
|
214
|
+
index on `(email,organization_id)`. In Brut:
|
215
|
+
```ruby {11}
|
216
|
+
Sequel.migration do
|
217
|
+
up do
|
218
|
+
create_table :accounts,
|
219
|
+
comment: "People or systems who can access this system",
|
220
|
+
external_id: true do
|
221
|
+
|
222
|
+
column :email, :text, unique: true
|
223
|
+
foreign_key :organization_id, :organizations
|
224
|
+
column :deactivated_at, :timestamptz, null: true
|
225
|
+
|
226
|
+
key [:email, :organization_id]
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
This allows your migrations to be more expressive *and* make it easier to set up unique constraints that relate
|
234
|
+
to your business logic or domain.
|
235
|
+
|
236
|
+
## Testing
|
237
|
+
|
238
|
+
Generally, you don't test database migrations, however you may want to test constraints or other logic you have
|
239
|
+
set up. Techniques for doing this are in the [database access](/database-access#testing) section.
|
240
|
+
|
241
|
+
## Recommended Practices
|
242
|
+
|
243
|
+
### Ephemeral Dev Database
|
244
|
+
|
245
|
+
Brut intends for your develompent database to be ephemeral. Your entire workflow should be built around it
|
246
|
+
being OK and normal to completely blow away your development database and recreate it. This is why down
|
247
|
+
migrations (and the use of `change`) are discouraged. You really don't need them.
|
248
|
+
|
249
|
+
Assuming you have [seed data](/seed-data) set up properly, you can reliably reset everything like so:
|
250
|
+
|
251
|
+
```
|
252
|
+
> bin/db rebuild
|
253
|
+
> bin/db seed
|
254
|
+
> bin/db rebuild -e test
|
255
|
+
```
|
256
|
+
|
257
|
+
As long as you don't change migrations that have been applied in production, you can safely run the above
|
258
|
+
commands to iterate on a schema change.
|
259
|
+
|
260
|
+
This does imply that you should not run business logic or make *data* changes in your migration files. No migration
|
261
|
+
should rely on specific data being in the database.
|
262
|
+
|
263
|
+
This workflow my be much different from what you are used to, but you will be quite happy when you adopt it. The days
|
264
|
+
of downloading a carefully-curated database image and taking great care to never delete it are over.
|
265
|
+
|
266
|
+
### Use Your Database, It is Awesome
|
267
|
+
|
268
|
+
Your database is the only part of the system that has any chance of ensuring data integrity. You can use
|
269
|
+
constraints, types, foreign keys, etc. to ensure that the data in your database is correct, based on your current
|
270
|
+
understandings. Code-based validation systems **cannot achieve this on any level**.
|
271
|
+
|
272
|
+
Thus, you are encouraged to learn about your database's features and use them!
|
273
|
+
|
274
|
+
For example, here's a way to add full text search to an existing table in Postgres:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
add_column :full_text_search,
|
278
|
+
:tsvector,
|
279
|
+
generated_always_as: Sequel.lit(%{
|
280
|
+
(
|
281
|
+
setweight(to_tsvector('english', name),'A') ||
|
282
|
+
setweight(to_tsvector('english', coalesce(description,'')),'B')
|
283
|
+
)
|
284
|
+
}),
|
285
|
+
generated_type: :stored
|
286
|
+
```
|
287
|
+
|
288
|
+
If you are using Postgtes, why *not* use its features? Unless your app is database-agnostic, you should be using
|
289
|
+
the features of your database, even if they aren't explicitly exposed via Sequel's Ruby API (that's why `Sequel.lit` exists).
|
290
|
+
|
291
|
+
## Technical Notes
|
292
|
+
|
293
|
+
> [!IMPORTANT]
|
294
|
+
> Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
|
295
|
+
> internals, the source code is always more correct.
|
296
|
+
|
297
|
+
_Last Updated May 8, 2025_
|
298
|
+
|
299
|
+
As mentioned, Brut uses Sequel under the covers. This is unlikely to change.
|
300
|
+
|
301
|
+
As also mentioned, Brut's extensions often rely on Postgres. While we can all dream of a world where every
|
302
|
+
developer uses the same database server, we don't live in that world. Brut should, some day, support all the
|
303
|
+
databases that Sequel supports. For now, however, it only supports Postgres.
|
304
|
+
|
305
|
+
This hard-coded support is due to:
|
306
|
+
|
307
|
+
* `pg_array`
|
308
|
+
* `pg_json`
|
309
|
+
* Reliance on `citext` and `comment`
|
310
|
+
* Reliance on `timestamptz`
|
311
|
+
|
312
|
+
Brut is likely to add more Postgres-specific features before adding support for other databases.
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Deployment
|
2
|
+
|
3
|
+
Brut apps are Rack apps, so they can be deployed in conventional
|
4
|
+
ways. Brut apps are 12-factor apps and the scripts used for
|
5
|
+
development are inteded to work for production as well.
|
6
|
+
|
7
|
+
## Overview
|
8
|
+
|
9
|
+
Everyone deploys apps in different ways. Brut can't provide a
|
10
|
+
simple solution for all deployment setups, so this document will
|
11
|
+
outline considerations when setting up deployment.
|
12
|
+
|
13
|
+
The most direct way to understand what needs to happen is to look
|
14
|
+
at `deploy/Dockerfile`, which is the foundation of a `Dockerfile`
|
15
|
+
you can use. In particular, it shows you the commands needed to
|
16
|
+
setup and run the app in production:
|
17
|
+
|
18
|
+
Beyond installing system software to run any Ruby web app, as well
|
19
|
+
as whatever is needed for NodeJS and Postgres, the Brut-specific
|
20
|
+
parts look like so:
|
21
|
+
|
22
|
+
1. Install Ruby Gems with `bundle install`
|
23
|
+
2. Install Node modules with `npm clean-install`
|
24
|
+
3. Build all assets with `bin/build-assets` (this will bundle all
|
25
|
+
CSS and Javascript, plus copy over any other [assets](/assets)
|
26
|
+
to the locations from where the Brut app will serve them)
|
27
|
+
4. Run the app with `bin/run`
|
28
|
+
|
29
|
+
Your Brut app also includes `bin/release` which is a script
|
30
|
+
intended to run in the production environment after the code has
|
31
|
+
been deployed, but before the app starts up. By default, it
|
32
|
+
applies any needed migrations to the database.
|
33
|
+
|
34
|
+
## Testing
|
35
|
+
|
36
|
+
If you are using Docker, you can create the `Dockerfile`s and run
|
37
|
+
them locally to see how they work. You will need to have local
|
38
|
+
versions of all infrastructure (database, Redis, etc.), but if
|
39
|
+
these work locally, there is a high chance they work in
|
40
|
+
production.
|
41
|
+
|
42
|
+
If you are not using Docker, you will need to apply various
|
43
|
+
techniques that are beyond the scope of this documentation.
|
44
|
+
|
45
|
+
## Recommended Practices
|
46
|
+
|
47
|
+
Brut goes to great lengths to avoid environment-specific code.
|
48
|
+
Much of Brut's behavior works the same in dev as it does in
|
49
|
+
production. For example, assets are hashed in all environments.
|
50
|
+
|
51
|
+
Assuming your code does the same thing, there should be a minium
|
52
|
+
of surprises. That all being said, here are some recommendations:
|
53
|
+
|
54
|
+
* Create a way to interact with external services in a testing
|
55
|
+
capacity. For example, ensure you have a test user with a known
|
56
|
+
email address and trigger an email to them. Or a company credit
|
57
|
+
card you charge and refund.
|
58
|
+
* Configure observability so you know what your app is doing at
|
59
|
+
all times.
|
60
|
+
* Configure a URL that, when accessed, produces an error. This
|
61
|
+
allows you to check your error reporting system.
|
62
|
+
* Create a page somewhere that shows the git SHA of your
|
63
|
+
deployment, or some other unique, unambiguous version number. This
|
64
|
+
will clarify what version of the code is actually running.
|
65
|
+
|
66
|
+
|