ekwo-os 0.4.1
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.
- package/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- package/schema/output.1.json +613 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
-- Ekwo OS — an append-only record of what was changed, by whom, and when.
|
|
2
|
+
--
|
|
3
|
+
-- Everything the ledger does is already immutable: an entry is posted once and
|
|
4
|
+
-- corrected by a reversal, never edited. What sat outside that guarantee is
|
|
5
|
+
-- everything *around* the ledger — the chart of accounts, the journals, the
|
|
6
|
+
-- taxes and the accounts they post to, the bank accounts, the contacts, the
|
|
7
|
+
-- products, the financial years, who is a member of a company and with which
|
|
8
|
+
-- role, which pack version the company holds. Those decide how every future
|
|
9
|
+
-- entry is booked, and until this migration nothing recorded that one of them
|
|
10
|
+
-- had moved. An auditor asking "who changed the VAT account on this tax, and
|
|
11
|
+
-- when" had no answer, and neither did the operator.
|
|
12
|
+
--
|
|
13
|
+
-- This is one table, one trigger function and a handful of triggers. It is
|
|
14
|
+
-- deliberately not `pgaudit`: that extension is unavailable under PGlite, so
|
|
15
|
+
-- none of this could be tested here, and its output goes to the Postgres log
|
|
16
|
+
-- — a file an application cannot query and a self-hosted operator often
|
|
17
|
+
-- cannot reach. An audit trail nobody can read is a promise, not a control.
|
|
18
|
+
--
|
|
19
|
+
-- Four decisions are worth stating.
|
|
20
|
+
--
|
|
21
|
+
-- **Append-only is enforced by a trigger, not only by row level security.**
|
|
22
|
+
-- Policies do not apply to the table owner, and on Supabase `service_role`
|
|
23
|
+
-- carries BYPASSRLS. A `before update or delete` trigger that raises holds for
|
|
24
|
+
-- everyone, owner included; the dated purge function lifts it for the duration
|
|
25
|
+
-- of its own transaction and for nothing else.
|
|
26
|
+
--
|
|
27
|
+
-- **`company_id` carries no foreign key.** The audit trail outlives the rows
|
|
28
|
+
-- it describes: a cascade from `companies` would delete the record of the
|
|
29
|
+
-- company's own deletion. It is indexed, and row level security reads it.
|
|
30
|
+
--
|
|
31
|
+
-- **The ledger is not audited.** `entries` and `entry_lines` are immutable
|
|
32
|
+
-- once posted and are corrected by a reversal, which is already a visible
|
|
33
|
+
-- act. What is recorded here is the *act* of posting, cancelling or reversing
|
|
34
|
+
-- — never the content of the entry, which the ledger itself holds.
|
|
35
|
+
--
|
|
36
|
+
-- **The installer's own bulk copy is not audited row by row.** Installing a
|
|
37
|
+
-- country pack copies a thousand accounts into a company; a thousand rows
|
|
38
|
+
-- saying "account created" carry no information the single `company_packs`
|
|
39
|
+
-- row does not. `is_installer()` is already the schema's name for the
|
|
40
|
+
-- migration runner, the seeds and `ekwo init`, so the trigger stands down for
|
|
41
|
+
-- it and for nothing else. A person, a machine key, or anyone connecting with
|
|
42
|
+
-- `psql` is audited.
|
|
43
|
+
|
|
44
|
+
-- ---------------------------------------------------------------------------
|
|
45
|
+
-- The table
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
create type audit_operation as enum ('insert', 'update', 'delete');
|
|
49
|
+
|
|
50
|
+
comment on type audit_operation is
|
|
51
|
+
'What happened to the row. The business act, when there is one, is named in audit_log.action.';
|
|
52
|
+
|
|
53
|
+
create table audit_log (
|
|
54
|
+
id bigint generated always as identity primary key,
|
|
55
|
+
occurred_at timestamptz not null default now(),
|
|
56
|
+
-- Who. `auth.uid()` for a person; a machine key answers with its own id and
|
|
57
|
+
-- no user, which is exactly the distinction an auditor needs.
|
|
58
|
+
actor_id uuid,
|
|
59
|
+
api_key_id uuid,
|
|
60
|
+
-- Which company the change belongs to. Null for the reference data of the
|
|
61
|
+
-- installation itself — the packs it holds, its administrators.
|
|
62
|
+
company_id uuid,
|
|
63
|
+
-- What.
|
|
64
|
+
table_name text not null,
|
|
65
|
+
record_id uuid,
|
|
66
|
+
record_key text not null,
|
|
67
|
+
operation audit_operation not null,
|
|
68
|
+
-- The business act, where the row change is one: `document_posted`,
|
|
69
|
+
-- `fiscal_year_closed`, `pack_upgraded`. Null for an ordinary edit.
|
|
70
|
+
action text,
|
|
71
|
+
old_values jsonb,
|
|
72
|
+
new_values jsonb,
|
|
73
|
+
constraint audit_log_has_a_subject check (length(table_name) > 0 and length(record_key) > 0)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
comment on table audit_log is
|
|
77
|
+
'Append-only record of every change to the configuration and reference data of a company, and of the acts that change the state of a document, a payment, a financial year or a pack. Written by trigger, never by a client; no update and no delete, for anyone.';
|
|
78
|
+
comment on column audit_log.actor_id is 'auth.uid() at the time of the change. Null when the change came from a machine key or from a direct connection.';
|
|
79
|
+
comment on column audit_log.api_key_id is 'The machine key presented in the transaction, when one was.';
|
|
80
|
+
comment on column audit_log.company_id is 'The company the change belongs to, and what row level security reads. No foreign key: the trail outlives the row it describes.';
|
|
81
|
+
comment on column audit_log.record_key is 'The natural key of the row — an account code, a country, a user id — so a deleted row is still identifiable.';
|
|
82
|
+
comment on column audit_log.action is 'The business act this change is, when it is one. Null for an ordinary edit.';
|
|
83
|
+
comment on column audit_log.old_values is 'The row before, as jsonb. Null on an insert. Secrets are replaced by null, never stored twice.';
|
|
84
|
+
comment on column audit_log.new_values is 'The row after, as jsonb. Null on a delete.';
|
|
85
|
+
|
|
86
|
+
create index audit_log_company_idx on audit_log (company_id, occurred_at desc);
|
|
87
|
+
create index audit_log_table_idx on audit_log (table_name, occurred_at desc);
|
|
88
|
+
create index audit_log_actor_idx on audit_log (actor_id, occurred_at desc);
|
|
89
|
+
create index audit_log_record_idx on audit_log (table_name, record_id);
|
|
90
|
+
|
|
91
|
+
-- ---------------------------------------------------------------------------
|
|
92
|
+
-- Append-only, for everyone
|
|
93
|
+
-- ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
create or replace function audit_log_is_append_only()
|
|
96
|
+
returns trigger
|
|
97
|
+
language plpgsql
|
|
98
|
+
as $$
|
|
99
|
+
begin
|
|
100
|
+
if current_setting('ekwo.audit_purge', true) = 'on' and tg_op = 'DELETE' then
|
|
101
|
+
return old;
|
|
102
|
+
end if;
|
|
103
|
+
raise exception 'audit_log_append_only: the audit trail is written once. % is refused; rows leave it only through purge_audit_log(date).', lower(tg_op);
|
|
104
|
+
end;
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
comment on function audit_log_is_append_only() is
|
|
108
|
+
'Refuses every update and every delete on audit_log, table owner included. purge_audit_log() sets ekwo.audit_purge for its own transaction, which is the one exception.';
|
|
109
|
+
|
|
110
|
+
create trigger audit_log_append_only
|
|
111
|
+
before update or delete on audit_log
|
|
112
|
+
for each row execute function audit_log_is_append_only();
|
|
113
|
+
|
|
114
|
+
-- ---------------------------------------------------------------------------
|
|
115
|
+
-- Writing a row
|
|
116
|
+
--
|
|
117
|
+
-- One function, `security definer`, so the triggers insert regardless of the
|
|
118
|
+
-- policies of the table they hang on and regardless of the fact that
|
|
119
|
+
-- `audit_log` has no insert policy at all. Nothing else may call it: the
|
|
120
|
+
-- grants at the end of this file close it to everyone but the owner.
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
create or replace function audit_record(
|
|
124
|
+
p_company_id uuid,
|
|
125
|
+
p_table text,
|
|
126
|
+
p_record_id uuid,
|
|
127
|
+
p_record_key text,
|
|
128
|
+
p_operation audit_operation,
|
|
129
|
+
p_action text default null,
|
|
130
|
+
p_old jsonb default null,
|
|
131
|
+
p_new jsonb default null
|
|
132
|
+
)
|
|
133
|
+
returns bigint
|
|
134
|
+
language plpgsql
|
|
135
|
+
security definer
|
|
136
|
+
set search_path = public, pg_temp
|
|
137
|
+
as $$
|
|
138
|
+
declare
|
|
139
|
+
v_id bigint;
|
|
140
|
+
begin
|
|
141
|
+
insert into audit_log (actor_id, api_key_id, company_id, table_name, record_id,
|
|
142
|
+
record_key, operation, action, old_values, new_values)
|
|
143
|
+
values (auth.uid(), (select id from current_api_key()), p_company_id, p_table,
|
|
144
|
+
p_record_id, p_record_key, p_operation, p_action, p_old, p_new)
|
|
145
|
+
returning id into v_id;
|
|
146
|
+
return v_id;
|
|
147
|
+
end;
|
|
148
|
+
$$;
|
|
149
|
+
|
|
150
|
+
comment on function audit_record(uuid, text, uuid, text, audit_operation, text, jsonb, jsonb) is
|
|
151
|
+
'Writes one row of the audit trail. Called by the triggers of this schema and by the functions that perform an act; never by a client.';
|
|
152
|
+
|
|
153
|
+
-- ---------------------------------------------------------------------------
|
|
154
|
+
-- The generic trigger
|
|
155
|
+
--
|
|
156
|
+
-- Configured by one jsonb argument rather than by a row of positional ones,
|
|
157
|
+
-- because a trigger declaration is read far more often than it is written:
|
|
158
|
+
--
|
|
159
|
+
-- company the column holding the company, or absent for reference data of
|
|
160
|
+
-- the installation itself
|
|
161
|
+
-- key the columns that make the natural key, joined by `/`
|
|
162
|
+
-- redact columns whose value is replaced by null — a secret is stored
|
|
163
|
+
-- once, in the table that owns it, and never a second time here
|
|
164
|
+
-- action_insert / action_delete
|
|
165
|
+
-- the name of the act, where an insert or a delete *is* one
|
|
166
|
+
-- ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
create or replace function audit_changes()
|
|
169
|
+
returns trigger
|
|
170
|
+
language plpgsql
|
|
171
|
+
security definer
|
|
172
|
+
set search_path = public, pg_temp
|
|
173
|
+
as $$
|
|
174
|
+
declare
|
|
175
|
+
v_config jsonb := coalesce(tg_argv[0], '{}')::jsonb;
|
|
176
|
+
v_old jsonb := case when tg_op = 'INSERT' then null else to_jsonb(old) end;
|
|
177
|
+
v_new jsonb := case when tg_op = 'DELETE' then null else to_jsonb(new) end;
|
|
178
|
+
v_subject jsonb := coalesce(v_new, v_old);
|
|
179
|
+
v_company uuid;
|
|
180
|
+
v_column text;
|
|
181
|
+
v_key text := '';
|
|
182
|
+
v_action text;
|
|
183
|
+
begin
|
|
184
|
+
-- The installation setting itself up. See the head of this file.
|
|
185
|
+
if is_installer() then
|
|
186
|
+
return null;
|
|
187
|
+
end if;
|
|
188
|
+
|
|
189
|
+
-- `updated_at` is the clock, not a change: `occurred_at` on the audit row
|
|
190
|
+
-- already says when. It is dropped from both sides so that an update which
|
|
191
|
+
-- moved nothing else compares equal.
|
|
192
|
+
v_old := v_old - 'updated_at';
|
|
193
|
+
v_new := v_new - 'updated_at';
|
|
194
|
+
|
|
195
|
+
-- An update that changed nothing is not a change. Re-applying an idempotent
|
|
196
|
+
-- seed upserts every row of the reference data; without this, `ekwo migrate`
|
|
197
|
+
-- would write a page of audit rows saying that nothing happened.
|
|
198
|
+
if tg_op = 'UPDATE' and v_old = v_new then
|
|
199
|
+
return null;
|
|
200
|
+
end if;
|
|
201
|
+
|
|
202
|
+
if v_config ? 'company' then
|
|
203
|
+
v_company := nullif(v_subject ->> (v_config ->> 'company'), '')::uuid;
|
|
204
|
+
end if;
|
|
205
|
+
|
|
206
|
+
for v_column in select jsonb_array_elements_text(coalesce(v_config -> 'key', '["id"]'::jsonb)) loop
|
|
207
|
+
v_key := case when v_key = '' then '' else v_key || '/' end
|
|
208
|
+
|| coalesce(v_subject ->> v_column, '');
|
|
209
|
+
end loop;
|
|
210
|
+
-- A draft carries no number yet, and a row identified by nothing is a row
|
|
211
|
+
-- nobody can find again.
|
|
212
|
+
if v_key = '' or v_key = '/' then
|
|
213
|
+
v_key := coalesce(v_subject ->> 'id', '(unkeyed)');
|
|
214
|
+
end if;
|
|
215
|
+
|
|
216
|
+
for v_column in select jsonb_array_elements_text(coalesce(v_config -> 'redact', '[]'::jsonb)) loop
|
|
217
|
+
if v_old is not null and v_old ? v_column then v_old := jsonb_set(v_old, array[v_column], 'null'::jsonb); end if;
|
|
218
|
+
if v_new is not null and v_new ? v_column then v_new := jsonb_set(v_new, array[v_column], 'null'::jsonb); end if;
|
|
219
|
+
end loop;
|
|
220
|
+
|
|
221
|
+
v_action := case tg_op
|
|
222
|
+
when 'INSERT' then v_config ->> 'action_insert'
|
|
223
|
+
when 'DELETE' then v_config ->> 'action_delete'
|
|
224
|
+
else null
|
|
225
|
+
end;
|
|
226
|
+
|
|
227
|
+
perform audit_record(
|
|
228
|
+
v_company,
|
|
229
|
+
tg_table_name,
|
|
230
|
+
case when v_subject ? 'id' and (v_subject ->> 'id') ~ '^[0-9a-f]{8}-' then (v_subject ->> 'id')::uuid end,
|
|
231
|
+
v_key,
|
|
232
|
+
lower(tg_op)::audit_operation,
|
|
233
|
+
v_action,
|
|
234
|
+
v_old,
|
|
235
|
+
v_new
|
|
236
|
+
);
|
|
237
|
+
return null;
|
|
238
|
+
end;
|
|
239
|
+
$$;
|
|
240
|
+
|
|
241
|
+
comment on function audit_changes() is
|
|
242
|
+
'The generic audit trigger. One jsonb argument names the company column, the natural key, the columns to redact and the acts an insert or a delete stands for.';
|
|
243
|
+
|
|
244
|
+
-- ---------------------------------------------------------------------------
|
|
245
|
+
-- The state-change trigger
|
|
246
|
+
--
|
|
247
|
+
-- The act, not the content. A document that is posted, a payment that is
|
|
248
|
+
-- booked, a financial year that closes: what is recorded is the transition and
|
|
249
|
+
-- the few fields that identify the row, never the whole of it.
|
|
250
|
+
--
|
|
251
|
+
-- watch the column whose change is the act
|
|
252
|
+
-- prefix the act is `<prefix>_<new value>`: document_posted, payment_cancelled
|
|
253
|
+
-- true / false
|
|
254
|
+
-- for a boolean column, the name of each direction
|
|
255
|
+
-- fields the columns copied into the payload beside the watched one
|
|
256
|
+
-- ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
create or replace function audit_state_change()
|
|
259
|
+
returns trigger
|
|
260
|
+
language plpgsql
|
|
261
|
+
security definer
|
|
262
|
+
set search_path = public, pg_temp
|
|
263
|
+
as $$
|
|
264
|
+
declare
|
|
265
|
+
v_config jsonb := coalesce(tg_argv[0], '{}')::jsonb;
|
|
266
|
+
v_watch text := v_config ->> 'watch';
|
|
267
|
+
v_old jsonb := to_jsonb(old);
|
|
268
|
+
v_new jsonb := to_jsonb(new);
|
|
269
|
+
v_before text := v_old ->> v_watch;
|
|
270
|
+
v_after text := v_new ->> v_watch;
|
|
271
|
+
v_action text;
|
|
272
|
+
v_column text;
|
|
273
|
+
v_payload jsonb := '{}'::jsonb;
|
|
274
|
+
begin
|
|
275
|
+
if v_before is not distinct from v_after then
|
|
276
|
+
return null;
|
|
277
|
+
end if;
|
|
278
|
+
|
|
279
|
+
if v_config ? 'true' then
|
|
280
|
+
v_action := case when v_after = 'true' then v_config ->> 'true' else v_config ->> 'false' end;
|
|
281
|
+
else
|
|
282
|
+
v_action := (v_config ->> 'prefix') || '_' || v_after;
|
|
283
|
+
end if;
|
|
284
|
+
|
|
285
|
+
for v_column in select jsonb_array_elements_text(coalesce(v_config -> 'fields', '[]'::jsonb)) loop
|
|
286
|
+
v_payload := v_payload || jsonb_build_object(v_column, v_new -> v_column);
|
|
287
|
+
end loop;
|
|
288
|
+
|
|
289
|
+
perform audit_record(
|
|
290
|
+
nullif(v_new ->> coalesce(v_config ->> 'company', 'company_id'), '')::uuid,
|
|
291
|
+
tg_table_name,
|
|
292
|
+
(v_new ->> 'id')::uuid,
|
|
293
|
+
coalesce(nullif(v_new ->> coalesce(v_config ->> 'key', 'id'), ''), v_new ->> 'id', '(unkeyed)'),
|
|
294
|
+
'update',
|
|
295
|
+
v_action,
|
|
296
|
+
jsonb_build_object(v_watch, v_before),
|
|
297
|
+
v_payload || jsonb_build_object(v_watch, v_after)
|
|
298
|
+
);
|
|
299
|
+
return null;
|
|
300
|
+
end;
|
|
301
|
+
$$;
|
|
302
|
+
|
|
303
|
+
comment on function audit_state_change() is
|
|
304
|
+
'Records the act a state column stands for — a document posted, a payment booked, a year closed — with the fields that identify the row and never the whole of it.';
|
|
305
|
+
|
|
306
|
+
-- ---------------------------------------------------------------------------
|
|
307
|
+
-- Posting an entry, and reversing one
|
|
308
|
+
--
|
|
309
|
+
-- `entries` gets its own function because the ledger has a second act the
|
|
310
|
+
-- others do not: a reversal is an entry that points at the one it undoes, and
|
|
311
|
+
-- calling that "posted" would lose the only word an auditor is looking for.
|
|
312
|
+
-- ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
create or replace function audit_entry_posting()
|
|
315
|
+
returns trigger
|
|
316
|
+
language plpgsql
|
|
317
|
+
security definer
|
|
318
|
+
set search_path = public, pg_temp
|
|
319
|
+
as $$
|
|
320
|
+
begin
|
|
321
|
+
if old.state is not distinct from new.state then
|
|
322
|
+
return null;
|
|
323
|
+
end if;
|
|
324
|
+
|
|
325
|
+
perform audit_record(
|
|
326
|
+
new.company_id,
|
|
327
|
+
'entries',
|
|
328
|
+
new.id,
|
|
329
|
+
coalesce(new.number, new.id::text),
|
|
330
|
+
'update',
|
|
331
|
+
case
|
|
332
|
+
when new.state = 'posted' and new.reversed_entry_id is not null then 'entry_reversed'
|
|
333
|
+
else 'entry_' || new.state::text
|
|
334
|
+
end,
|
|
335
|
+
jsonb_build_object('state', old.state),
|
|
336
|
+
jsonb_build_object('state', new.state, 'number', new.number,
|
|
337
|
+
'entry_date', new.entry_date, 'kind', new.kind,
|
|
338
|
+
'reversed_entry_id', new.reversed_entry_id)
|
|
339
|
+
);
|
|
340
|
+
return null;
|
|
341
|
+
end;
|
|
342
|
+
$$;
|
|
343
|
+
|
|
344
|
+
comment on function audit_entry_posting() is
|
|
345
|
+
'Records that an entry was posted, cancelled, or posted as the reversal of another. The lines themselves are not audited: a posted entry is immutable and is corrected by a reversal.';
|
|
346
|
+
|
|
347
|
+
-- ---------------------------------------------------------------------------
|
|
348
|
+
-- What is covered
|
|
349
|
+
--
|
|
350
|
+
-- Everything that decides how a future entry is booked, plus who may book it.
|
|
351
|
+
-- ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
create trigger accounts_audit after insert or update or delete on accounts
|
|
354
|
+
for each row execute function audit_changes('{"company":"company_id","key":["code"]}');
|
|
355
|
+
|
|
356
|
+
create trigger journals_audit after insert or update or delete on journals
|
|
357
|
+
for each row execute function audit_changes('{"company":"company_id","key":["code"]}');
|
|
358
|
+
|
|
359
|
+
create trigger taxes_audit after insert or update or delete on taxes
|
|
360
|
+
for each row execute function audit_changes('{"company":"company_id","key":["code"]}');
|
|
361
|
+
|
|
362
|
+
create trigger tax_postings_audit after insert or update or delete on tax_postings
|
|
363
|
+
for each row execute function audit_changes('{"company":"company_id","key":["id"]}');
|
|
364
|
+
|
|
365
|
+
create trigger bank_accounts_audit after insert or update or delete on bank_accounts
|
|
366
|
+
for each row execute function audit_changes('{"company":"company_id","key":["id"]}');
|
|
367
|
+
|
|
368
|
+
create trigger contacts_audit after insert or update or delete on contacts
|
|
369
|
+
for each row execute function audit_changes('{"company":"company_id","key":["id"]}');
|
|
370
|
+
|
|
371
|
+
create trigger products_audit after insert or update or delete on products
|
|
372
|
+
for each row execute function audit_changes('{"company":"company_id","key":["code"]}');
|
|
373
|
+
|
|
374
|
+
create trigger companies_audit after insert or update or delete on companies
|
|
375
|
+
for each row execute function audit_changes('{"company":"id","key":["id"]}');
|
|
376
|
+
|
|
377
|
+
create trigger fiscal_years_audit after insert or delete on fiscal_years
|
|
378
|
+
for each row execute function audit_changes('{"company":"company_id","key":["name"]}');
|
|
379
|
+
|
|
380
|
+
create trigger company_members_audit after insert or update or delete on company_members
|
|
381
|
+
for each row execute function audit_changes('{"company":"company_id","key":["user_id"]}');
|
|
382
|
+
|
|
383
|
+
create trigger company_packs_audit after insert or update or delete on company_packs
|
|
384
|
+
for each row execute function audit_changes('{"company":"company_id","key":["country"]}');
|
|
385
|
+
|
|
386
|
+
-- The two tables that hold a secret. Only its hash is stored there, and not
|
|
387
|
+
-- even that is copied here: a hash is a credential that can be attacked
|
|
388
|
+
-- offline, and the audit trail is read by more people than the table is.
|
|
389
|
+
create trigger api_keys_audit after insert or update or delete on api_keys
|
|
390
|
+
for each row execute function audit_changes('{"company":"company_id","key":["prefix"],"redact":["key_hash"]}');
|
|
391
|
+
|
|
392
|
+
create trigger company_invitations_audit after insert or update or delete on company_invitations
|
|
393
|
+
for each row execute function audit_changes('{"company":"company_id","key":["email"],"redact":["token_hash"]}');
|
|
394
|
+
|
|
395
|
+
-- Reference data of the installation: no company, so only an instance
|
|
396
|
+
-- administrator reads these rows back.
|
|
397
|
+
create trigger instance_admins_audit after insert or update or delete on instance_admins
|
|
398
|
+
for each row execute function audit_changes('{"key":["user_id"]}');
|
|
399
|
+
|
|
400
|
+
create trigger country_packs_audit after insert or update or delete on country_packs
|
|
401
|
+
for each row execute function audit_changes('{"key":["country"]}');
|
|
402
|
+
|
|
403
|
+
create trigger country_defaults_audit after insert or update or delete on country_defaults
|
|
404
|
+
for each row execute function audit_changes('{"key":["country"]}');
|
|
405
|
+
|
|
406
|
+
-- ---------------------------------------------------------------------------
|
|
407
|
+
-- The acts
|
|
408
|
+
-- ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
create trigger documents_audit_state after update on documents
|
|
411
|
+
for each row execute function audit_state_change(
|
|
412
|
+
'{"watch":"state","prefix":"document","key":"number","fields":["doc_type","document_date","amount_total","entry_id","contact_id"]}');
|
|
413
|
+
|
|
414
|
+
create trigger payments_audit_state after update on payments
|
|
415
|
+
for each row execute function audit_state_change(
|
|
416
|
+
'{"watch":"state","prefix":"payment","fields":["payment_date","amount","entry_id","contact_id"]}');
|
|
417
|
+
|
|
418
|
+
create trigger fiscal_years_audit_close after update on fiscal_years
|
|
419
|
+
for each row execute function audit_state_change(
|
|
420
|
+
'{"watch":"is_closed","true":"fiscal_year_closed","false":"fiscal_year_reopened","key":"name","fields":["start_date","end_date"]}');
|
|
421
|
+
|
|
422
|
+
create trigger entries_audit_posting after update on entries
|
|
423
|
+
for each row execute function audit_entry_posting();
|
|
424
|
+
|
|
425
|
+
-- Matching and unmatching a payment: an insert and a delete that are each an
|
|
426
|
+
-- act, so the generic trigger names them.
|
|
427
|
+
create trigger reconciliations_audit after insert or delete on reconciliations
|
|
428
|
+
for each row execute function audit_changes(
|
|
429
|
+
'{"company":"company_id","key":["id"],"action_insert":"payment_reconciled","action_delete":"payment_unreconciled"}');
|
|
430
|
+
|
|
431
|
+
-- ---------------------------------------------------------------------------
|
|
432
|
+
-- The purge
|
|
433
|
+
--
|
|
434
|
+
-- A retention policy is a legal decision, not a default: nothing in this
|
|
435
|
+
-- schema deletes an audit row on its own. `purge_audit_log` takes the cutoff
|
|
436
|
+
-- it is asked for, deletes what is strictly older, and is reachable only by
|
|
437
|
+
-- `service_role` — the operator of the installation, never a signed-in user
|
|
438
|
+
-- and never a machine key.
|
|
439
|
+
-- ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
create or replace function purge_audit_log(p_before date)
|
|
442
|
+
returns bigint
|
|
443
|
+
language plpgsql
|
|
444
|
+
security definer
|
|
445
|
+
set search_path = public, pg_temp
|
|
446
|
+
as $$
|
|
447
|
+
declare
|
|
448
|
+
v_deleted bigint;
|
|
449
|
+
begin
|
|
450
|
+
if p_before is null then
|
|
451
|
+
raise exception 'audit_purge_needs_a_date: name the date before which rows are dropped. There is no default retention.';
|
|
452
|
+
end if;
|
|
453
|
+
if p_before > current_date then
|
|
454
|
+
raise exception 'audit_purge_in_the_future: % is not in the past, so this would empty the trail.', p_before;
|
|
455
|
+
end if;
|
|
456
|
+
|
|
457
|
+
perform set_config('ekwo.audit_purge', 'on', true);
|
|
458
|
+
delete from audit_log where occurred_at < p_before::timestamptz;
|
|
459
|
+
get diagnostics v_deleted = row_count;
|
|
460
|
+
perform set_config('ekwo.audit_purge', '', true);
|
|
461
|
+
|
|
462
|
+
perform audit_record(null, 'audit_log', null, p_before::text, 'delete', 'audit_log_purged',
|
|
463
|
+
null, jsonb_build_object('before', p_before, 'rows', v_deleted));
|
|
464
|
+
return v_deleted;
|
|
465
|
+
end;
|
|
466
|
+
$$;
|
|
467
|
+
|
|
468
|
+
comment on function purge_audit_log(date) is
|
|
469
|
+
'Drops audit rows older than a date the caller names, and records that it did. service_role only: retention is the operator''s decision and no signed-in user may make it.';
|
|
470
|
+
|
|
471
|
+
-- ---------------------------------------------------------------------------
|
|
472
|
+
-- Row level security
|
|
473
|
+
--
|
|
474
|
+
-- Select only, and there is no other policy on purpose: the trail is written
|
|
475
|
+
-- by `security definer` triggers that run as the owner, so a client has no
|
|
476
|
+
-- reason to hold an insert policy and every reason not to.
|
|
477
|
+
-- ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
alter table audit_log enable row level security;
|
|
480
|
+
|
|
481
|
+
create policy audit_log_select on audit_log
|
|
482
|
+
for select using (
|
|
483
|
+
case when company_id is null then is_instance_admin()
|
|
484
|
+
else is_company_member(company_id) or is_instance_admin()
|
|
485
|
+
end
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
comment on policy audit_log_select on audit_log is
|
|
489
|
+
'Members of the company read its trail; an instance administrator reads that and the rows of the installation itself. Nobody writes, updates or deletes: there is no policy for it, and a trigger refuses it to the owner too.';
|
|
490
|
+
|
|
491
|
+
-- ---------------------------------------------------------------------------
|
|
492
|
+
-- Grants
|
|
493
|
+
--
|
|
494
|
+
-- The rule of `supabase/migrations/README.md`: a migration that adds a
|
|
495
|
+
-- function closes the schema behind it, from PUBLIC and never from `anon`.
|
|
496
|
+
-- Then the two functions that are not the triggers' own get their audience.
|
|
497
|
+
-- ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
revoke execute on all functions in schema public from public;
|
|
500
|
+
|
|
501
|
+
revoke execute on function audit_record(uuid, text, uuid, text, audit_operation, text, jsonb, jsonb)
|
|
502
|
+
from public, anon, authenticated;
|
|
503
|
+
revoke execute on function audit_changes() from public, anon, authenticated;
|
|
504
|
+
revoke execute on function audit_state_change() from public, anon, authenticated;
|
|
505
|
+
revoke execute on function audit_entry_posting() from public, anon, authenticated;
|
|
506
|
+
revoke execute on function audit_log_is_append_only() from public, anon, authenticated;
|
|
507
|
+
|
|
508
|
+
revoke execute on function purge_audit_log(date) from public, anon, authenticated;
|
|
509
|
+
grant execute on function purge_audit_log(date) to service_role;
|