pgkiln 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/SECURITY.md +10 -1
  3. package/db/migrations/121_ai_agents.sql +94 -0
  4. package/db/migrations/122_team_development.sql +186 -0
  5. package/db/migrations/123_workflow_outcomes.sql +407 -0
  6. package/db/migrations/124_component_details.sql +51 -0
  7. package/db/migrations/125_app_change.sql +23 -0
  8. package/db/migrations/126_mail_queue_network_access.sql +119 -0
  9. package/docs/apex-feature-parity.md +50 -19
  10. package/docs/guide/01-installation.md +8 -4
  11. package/docs/guide/03-builder.md +197 -10
  12. package/docs/guide/04-pages-and-regions.md +71 -21
  13. package/docs/guide/05-items.md +7 -1
  14. package/docs/guide/06-processing.md +129 -9
  15. package/docs/guide/07-dynamic-actions.md +34 -0
  16. package/docs/guide/08-security.md +55 -0
  17. package/docs/guide/09-reference.md +5 -2
  18. package/docs/guide/11-from-apex.md +3 -1
  19. package/docs/guide/12-development.md +87 -16
  20. package/docs/guide/14-globalization.md +49 -1
  21. package/docs/guide/18-cli.md +21 -1
  22. package/docs/guide/19-rest-data-sources.md +12 -0
  23. package/docs/guide/20-ai-agents.md +12 -4
  24. package/examples/hr/README.md +2 -0
  25. package/examples/hr/hr_64_ai_agent.sql +97 -0
  26. package/examples/hr/hr_65_ask_reports.sql +21 -0
  27. package/examples/hr/hr_66_team_development.sql +58 -0
  28. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  29. package/examples/hr/hr_68_component_details.sql +96 -0
  30. package/examples/hr/hr_70_data_security.sql +79 -0
  31. package/package.json +1 -1
  32. package/public/app.css +135 -21
  33. package/public/app.js +134 -20
  34. package/public/builder.css +133 -1
  35. package/public/builder.js +51 -0
  36. package/src/ai/chat.ts +18 -18
  37. package/src/ai/service.ts +35 -11
  38. package/src/ai/types.ts +7 -2
  39. package/src/app.ts +15 -0
  40. package/src/builder/advisor.ts +6 -3
  41. package/src/builder/ai-agents.ts +372 -0
  42. package/src/builder/ai-builder.ts +3 -3
  43. package/src/builder/ai.ts +13 -9
  44. package/src/builder/app-updates.ts +55 -0
  45. package/src/builder/assistant.ts +22 -7
  46. package/src/builder/components.ts +38 -9
  47. package/src/builder/data-security.ts +182 -0
  48. package/src/builder/designer.ts +8 -3
  49. package/src/builder/feedback.ts +6 -1
  50. package/src/builder/home.ts +16 -5
  51. package/src/builder/locks.ts +7 -1
  52. package/src/builder/mail-queue.ts +212 -0
  53. package/src/builder/mail.ts +72 -45
  54. package/src/builder/network-access.ts +140 -0
  55. package/src/builder/object-actions.ts +1 -0
  56. package/src/builder/object-browser.ts +1 -1
  57. package/src/builder/region-help.ts +2 -2
  58. package/src/builder/rest-catalogs.ts +19 -4
  59. package/src/builder/routes.ts +4 -1
  60. package/src/builder/shared.ts +1 -1
  61. package/src/builder/sql.ts +2 -0
  62. package/src/builder/team.ts +991 -0
  63. package/src/builder/themeroller.ts +47 -6
  64. package/src/builder/ui.ts +7 -1
  65. package/src/builder/websources.ts +1 -1
  66. package/src/builder/workflow-migrate.ts +210 -0
  67. package/src/builder/workflows.ts +29 -6
  68. package/src/builder/workspace-home.ts +2 -0
  69. package/src/builder/workspace-mail.ts +121 -0
  70. package/src/builder/workspace-transfer.ts +1 -1
  71. package/src/builder/workspaces.ts +1 -0
  72. package/src/cli/main.ts +25 -3
  73. package/src/cli/mcp.ts +40 -4
  74. package/src/cli/only.ts +52 -0
  75. package/src/cli/replace.ts +11 -1
  76. package/src/datasec.ts +343 -0
  77. package/src/i18n/ar.ts +17 -0
  78. package/src/i18n/cs.ts +17 -0
  79. package/src/i18n/da.ts +17 -0
  80. package/src/i18n/de.ts +17 -0
  81. package/src/i18n/el.ts +17 -0
  82. package/src/i18n/es.ts +17 -0
  83. package/src/i18n/fi.ts +17 -0
  84. package/src/i18n/fr.ts +17 -0
  85. package/src/i18n/he.ts +17 -0
  86. package/src/i18n/it.ts +17 -0
  87. package/src/i18n/ja.ts +17 -0
  88. package/src/i18n/ko.ts +17 -0
  89. package/src/i18n/nb.ts +17 -0
  90. package/src/i18n/pl.ts +17 -0
  91. package/src/i18n/pt.ts +17 -0
  92. package/src/i18n/ru.ts +17 -0
  93. package/src/i18n/sv.ts +17 -0
  94. package/src/i18n/tr.ts +17 -0
  95. package/src/i18n/uk.ts +17 -0
  96. package/src/i18n/zh.ts +17 -0
  97. package/src/i18n.ts +34 -0
  98. package/src/instance.ts +2 -0
  99. package/src/mail/mail.ts +50 -11
  100. package/src/mail/providers.ts +6 -3
  101. package/src/metadata.ts +11 -3
  102. package/src/network-acl.ts +126 -0
  103. package/src/objectstore.ts +3 -0
  104. package/src/runtime/ai-agents.ts +365 -0
  105. package/src/runtime/ai-filter.ts +385 -107
  106. package/src/runtime/assistant.ts +64 -11
  107. package/src/runtime/context.ts +2 -0
  108. package/src/runtime/engine.ts +3 -1
  109. package/src/runtime/ir-phrases.ts +812 -0
  110. package/src/runtime/ir-plan.ts +449 -0
  111. package/src/runtime/items.ts +1 -1
  112. package/src/runtime/regions.ts +6 -2
  113. package/src/runtime/render.ts +5 -3
  114. package/src/runtime/report.ts +1 -1
  115. package/src/runtime/styles.ts +54 -12
  116. package/src/runtime/tasks.ts +63 -12
  117. package/src/task-events.ts +232 -0
  118. package/src/team-samples.ts +74 -0
  119. package/src/team.ts +342 -0
  120. package/src/webclient.ts +45 -4
  121. package/src/webrequests.ts +5 -0
  122. package/src/websources.ts +30 -12
  123. package/src/workflow.ts +135 -18
  124. package/src/workspace-export.ts +16 -1
package/README.md CHANGED
@@ -123,7 +123,7 @@ developing pgkiln itself.
123
123
 
124
124
  ## What's in the box
125
125
 
126
- For a full comparison with Oracle APEX 26.1, including what's missing, see
126
+ For a full comparison with Oracle APEX 26.2, including what's missing, see
127
127
  **[docs/apex-feature-parity.md](docs/apex-feature-parity.md)**.
128
128
 
129
129
  **Runtime (Universal Theme-style UI, responsive on phone, tablet and desktop)**
package/SECURITY.md CHANGED
@@ -99,6 +99,15 @@ first version (MVP). Every finding below has a regression test in
99
99
  | SQL Worksheet transactions (sprint 107) | A worksheet's open transaction is held in server memory under the caller's builder session id plus the tab key and checked against the workspace: every lookup starts from the caller's own session, so another developer or another sign-in never reaches it. Its connection is a plain client built from the workspace's developer login (or the *Run as* application's), never returned to a pool, and closed at the end, so `SET ROLE` reaches only roles that login has and nothing leaks into the builder's pools; *Run as* can't change while it is open. Rolled back on idle timeout, sign-out, session expiry, workspace switch, tab close and server stop, with `idle_in_transaction_session_timeout` as a backstop; at most `WORKSHEET_TXN_MAX_PER_DEVELOPER` per developer and `WORKSHEET_TXN_MAX` per server, each one database connection outside the pools. CSRF and a developer sign-in on every endpoint. |
100
100
  | Release check (sprint 109) | Off by default (`PGKILN_RELEASE_CHECK`, or Instance settings for administrators). When on, the server sends one GET to GitHub's releases API at most once a day per process, with a 5-second timeout and a 1 MB limit, and sends nothing about the installation; only a semver version and an https link are kept from the answer, and they are escaped on the Workspace home. Pages never wait for it. |
101
101
  | Data Reporter datasets and reporting applications (sprint 110) | Datasets are managed by the workspace's developers with CSRF on every POST; their schema must be one the workspace's SQL Workshop connection may use (never pgkiln's own or the system's), and tables, columns, joins and filters are checked against the catalog. The runtime builds a dataset's SELECT with quoted identifiers, literal values and whitelisted operators, and runs reports as the application's role. A reporting application's role gets SELECT only on the datasets' tables and views, so RLS applies; viewers never reach the report editor (report parameters in the URL are ignored), and only `admin_authz` users unpublish or delete others' reports. Dataset files are checked like a saved dataset before import. |
102
+ | AI Agents and tools (sprint 111) | SQL tools are read-only (functions that change data and data-modifying `WITH` fail) and run as the application's role, so row level security applies; the model can't set `:APP_USER`. A Navigate tool links only to pages the user may open, with item values of that page and a checksum pgkiln builds. An agent's authorization scheme refuses other users before any AI call. Tool arguments and model output reach the page escaped. AI service keys stay on the server: not in builder pages, the tool call log, an export or the runtime role's reach. The builder's agent and tool forms need a developer and the CSRF token. |
103
+ | AI interactive reports (sprint 112) | "Ask in your own words" (the phrase parser and the AI answer) only sets the report's own settings over its shown columns, never a hidden one; operators, functions, colours, chart types and computed-column expressions come from whitelists and are checked; values are query parameters or escaped literals. Chips are edited and removed through a CSRF-checked form whose fields are checked again; a report without `ai_filter`, a region of another page and signed-out users are refused. |
104
+ | Team Development (sprint 113) | Issues, labels, milestones, templates and saved views belong to the developer's current workspace only, and can be neither read nor changed from another; assignees and @mentions are the workspace's developers, links go only to its applications. Every POST needs the session's CSRF token; titles, bodies, comments and names are escaped, Markdown is made safe, label colours come from a palette. Application users and signed-out visitors can't reach it. |
105
+ | Workflow 26.2 (sprint 114) | Migrating running instances needs a developer of the application and the CSRF token; unmapped steps and other applications' workflows are refused. A task ends only with an outcome of its definition. Task event SQL runs as the application's role (after the commit, or in the request for *fail* events). Draft versions can't be activated in the builder, with SQL or into the active steps. The diagram export is for developers only, served as a sandboxed attachment with escaped labels. |
106
+ | Component details (sprint 115) | The popup LOV search needs the CSRF token, caps the page size and page number and binds the search term. A wait popup's message is page data shown as text. A `menu_entry` dynamic action with an authorization the user lacks is neither sent to the page nor run (403); menu entries the user may not use are left out, labels are escaped, and the database refuses crafted action names. |
107
+ | App Builder list details (sprint 116) | `meta.app_change` (Updated by) is written only for a builder POST that passed the developer checks (sign-in, CSRF token, workspace, locks) and succeeded; posts that change nothing record nothing. Developer names and lock owners are escaped in the report, the cards and the lock label; another workspace's applications are not listed. The table is closed to the runtime role and not exported. |
108
+ | Pillar navigation appearances (sprint 117) | The appearance and the header strip colours are changed only by a developer with the CSRF token; only keys of a fixed list and `#rrggbb` colours are saved, and a hand-edited theme can't put markup or CSS into the page (a `<body>` class and the nonce'd `<style>`). A strip colour from an item is used only when the value is `#rrggbb`. |
109
+ | Mail queue, workspace mail and network access (sprint 118) | Administration → Mail queue: administrators see every workspace, a workspace's administrators only their current workspace's applications, other developers get 403; Retry and Purge (one mail or all failed) check the mail's workspace again on the server and need the CSRF token; bodies and attachments are never shown. A workspace's own mail service is managed by its administrators only; its secret is encrypted like a web credential's, write-only, never shown or exported (not in the workspace export); it has no API base URL or token URL, and its SMTP host must pass the installation's allow-list and the workspace's network access list and resolve to a public address (unless `PGKILN_REST_PRIVATE_HOSTS` lists it), resolved once and connected to by that address (no DNS rebinding). A workspace's network access list narrows `PGKILN_REST_ALLOWED_HOSTS` on the server for every outgoing web call of its applications (`meta.web_request`, REST data sources incl. write-back and the response cache, invoke API, token endpoints, object storage, the builder's Test, REST source catalogs), on every redirect hop; it can't widen the installation's list or the private-address checks. Refusals go to the debug log and `meta.network_refusal` (7 days). Also fixed: *REST source catalogs → read from a URL* fetched the URL before checking the developer's session (any visitor could make the server call an allowed host); it is now read only after the developer and CSRF checks. |
110
+ | Data security per table (sprint 119) | SQL Workshop → a table → Security writes `GRANT` / `REVOKE` and a masking view, so it needs a signed-in developer and the CSRF token, and works only on tables of the workspace's schemas (never pgkiln's own or the system's: 404). Table, column, view and role names reach SQL quoted; view names and application roles that could break out of their quotes are refused. The masking role is `nologin`, a member of the reader roles, so row level security still applies inside the view, and the generated SQL refuses to run when that role would bypass RLS. MCP `run_query` with an application runs as that application's database role (read-only) and refuses unknown applications. |
102
111
 
103
112
  ## Findings of the MVP review and their fixes
104
113
 
@@ -218,6 +227,6 @@ A second full review, mainly of what end users and API callers can reach. Regres
218
227
  5. Give each app its own `db_role` with the minimum grants; add RLS where rows are per user or per team. REST modules and AutoREST refuse to run without one on a privileged connection (403); run the Advisor after upgrading.
219
228
  6. Restrict network access to `/builder` (reverse proxy or firewall) if developers are a small group.
220
229
  7. With a REST API: set a long random `API_JWT_SECRET` (the same in PostgREST), change the `pgkiln_authenticator` password, configure `db-pre-request = meta.api_check`, expose only the `api` schema, and run PostgREST behind HTTPS.
221
- 8. With REST data sources: list only the hosts you need in `PGKILN_REST_ALLOWED_HOSTS` (avoid `*`), set a long random `PGKILN_SECRET_KEY` (32+ characters) kept outside the database and its backups, and use `PGKILN_REST_PRIVATE_HOSTS` only for internal services you mean to reach.
230
+ 8. With REST data sources: list only the hosts you need in `PGKILN_REST_ALLOWED_HOSTS` (avoid `*`), set a long random `PGKILN_SECRET_KEY` (32+ characters) kept outside the database and its backups, and use `PGKILN_REST_PRIVATE_HOSTS` only for internal services you mean to reach. Narrow it further per workspace under Administration → Network access (sprint 118).
222
231
  9. With header authentication: list only your proxy's addresses in `PGKILN_AUTH_HEADER_PROXIES`, make sure users can't reach pgkiln directly, and have the proxy remove any incoming copy of the header.
223
232
  10. With database-account authentication: list exact roles or use a dedicated membership role, and limit what those roles may connect to in `pg_hba.conf` (they can reach the database with their password outside pgkiln too).
@@ -0,0 +1,94 @@
1
+ -- =====================================================================
2
+ -- 121: AI agents and AI tools (sprint 111, WS-4; APEX 26.1 / 26.2: Shared
3
+ -- Components → AI Agents, AI Tools), more AI providers, and a log of
4
+ -- every tool call
5
+ --
6
+ -- meta.app.ai_tools: the application's AI tools, a JSON list. A tool is
7
+ -- what an agent may do, each one an approved, constrained action:
8
+ -- {"name": "top_earners", "type": "sql", "description": "…",
9
+ -- "sql": "select … where dname ilike :DNAME", "parameters": {"DNAME": {"type": "string"}},
10
+ -- "max_rows": 20, "timeout_s": 10, "authz": "MANAGER"}
11
+ -- Types: sql (a query, run as the application's role through RLS in a
12
+ -- read-only transaction with a time limit), function (a PL/pgSQL
13
+ -- function called with named arguments, as the application's role; it
14
+ -- may change data), rest (a REST data source of the application),
15
+ -- javascript (a function registered with pgkiln.actions.register and/or
16
+ -- the dynamic actions of the custom event with the tool's action name,
17
+ -- run in the user's browser after the answer) and navigate (the model
18
+ -- picks one of the tool's pages and item values; pgkiln checks the user
19
+ -- may open the page and builds the URL with its checksum).
20
+ -- meta.app.ai_agents: the application's AI agents, a JSON list:
21
+ -- {"name": "HR_AGENT", "description": "…", "service": "HR_ASSISTANT",
22
+ -- "system": "…", "effort": "low" | "medium" | "high" | null,
23
+ -- "tools": ["top_earners", "open_employee"], "max_steps": 5,
24
+ -- "max_tokens": 2000, "authz": "MANAGER"}
25
+ -- An AI assistant region names an agent ("agent" in its config) instead
26
+ -- of a service, a system prompt and tools of its own.
27
+ -- Both are definitions of the application: export_app copies every column
28
+ -- of meta.app, so they travel with it (no redefinition of export_app /
29
+ -- import_app). An export made before 121 has neither key: the trigger
30
+ -- below turns the NULL from jsonb_populate_record into an empty list.
31
+ -- The builder checks both lists (src/runtime/ai-agents.ts); the runtime
32
+ -- checks them again before it uses them.
33
+ --
34
+ -- meta.ai_service.provider: also "gemini" (Google Gemini through its
35
+ -- OpenAI-compatible endpoint) and "openai_compatible" (any endpoint that
36
+ -- speaks OpenAI's Chat Completions: Ollama, vLLM, Azure OpenAI, a
37
+ -- gateway). An OpenAI-compatible service needs a base URL; its key may
38
+ -- be empty (a local Ollama needs none).
39
+ --
40
+ -- meta.ai_tool_call: one row per tool call of an assistant or agent (app,
41
+ -- page, user, agent, tool, type, the arguments the model gave, status,
42
+ -- duration, a short message). The arguments may hold user data: only
43
+ -- the owner reads the table (Activity → AI usage); kept 30 days.
44
+ -- Installation data, like meta.ai_usage: not exported.
45
+ -- =====================================================================
46
+
47
+ alter table meta.app add column ai_tools jsonb not null default '[]'
48
+ check (jsonb_typeof(ai_tools) = 'array');
49
+ alter table meta.app add column ai_agents jsonb not null default '[]'
50
+ check (jsonb_typeof(ai_agents) = 'array');
51
+ comment on column meta.app.ai_tools is 'AI tools (121): what the application''s AI agents may do (sql, function, rest, javascript, navigate)';
52
+ comment on column meta.app.ai_agents is 'AI agents (121): AI service, system prompt, reasoning effort, tools, limits and authorization of each agent';
53
+
54
+ create function meta.app_ai_default() returns trigger
55
+ language plpgsql set search_path = pg_catalog as $$
56
+ begin
57
+ new.ai_tools := coalesce(new.ai_tools, '[]');
58
+ new.ai_agents := coalesce(new.ai_agents, '[]');
59
+ return new;
60
+ end $$;
61
+ create trigger app_ai_default before insert or update on meta.app
62
+ for each row execute function meta.app_ai_default();
63
+
64
+ alter table meta.ai_service drop constraint ai_service_provider_check;
65
+ alter table meta.ai_service add constraint ai_service_provider_check
66
+ check (provider in ('anthropic', 'openai', 'gemini', 'openai_compatible'));
67
+ alter table meta.ai_service add constraint ai_service_compatible_url
68
+ check (provider <> 'openai_compatible' or base_url is not null);
69
+ -- conversations keep the provider's format (Gemini and OpenAI-compatible: OpenAI's)
70
+ alter table meta.ai_conversation drop constraint ai_conversation_provider_check;
71
+ alter table meta.ai_conversation add constraint ai_conversation_provider_check
72
+ check (provider in ('anthropic', 'openai', 'gemini', 'openai_compatible'));
73
+
74
+ create table meta.ai_tool_call (
75
+ id bigserial primary key,
76
+ at timestamptz not null default now(),
77
+ app_id int not null references meta.app on delete cascade,
78
+ page_no int,
79
+ region_id int,
80
+ username text,
81
+ -- null: a tool of the region itself (no agent)
82
+ agent text,
83
+ tool text not null,
84
+ type text not null,
85
+ -- the model's arguments (checked), cut to a few kilobytes
86
+ arguments jsonb not null default '{}',
87
+ status text not null check (status in ('ok', 'error', 'refused')),
88
+ duration_ms int not null default 0,
89
+ message text
90
+ );
91
+ create index on meta.ai_tool_call (app_id, id desc);
92
+ create index on meta.ai_tool_call (at);
93
+ revoke all on meta.ai_tool_call from public;
94
+ comment on table meta.ai_tool_call is 'Tool calls of AI assistants and agents (121), with their arguments; kept 30 days';
@@ -0,0 +1,186 @@
1
+ -- =====================================================================
2
+ -- 122: Team Development (APEX: Team Development in the workspace)
3
+ --
4
+ -- An issue tracker per workspace for the team that builds its
5
+ -- applications: issues (title, Markdown body, open or closed) with labels,
6
+ -- a milestone, assignees (the workspace's developers), a link to an
7
+ -- application and one of its pages, comments with @mentions of developers
8
+ -- (shown in the builder, never mailed) and a history of changes. Labels
9
+ -- belong to label groups (e.g. Type: Bug, Feature, Task; Priority: High,
10
+ -- Medium, Low); templates prefill new issues (kind 'issue') or comments
11
+ -- (kind 'response', canned answers). Developers save their own filtered
12
+ -- views of the list.
13
+ --
14
+ -- An issue may come from a user's feedback (meta.feedback, Activity →
15
+ -- Feedback) or a developer comment (meta.dev_comment): linked back.
16
+ --
17
+ -- Workspace data, like the SQL Workshop's scripts: written by the builder
18
+ -- as the owner, never read by the runtime; not part of an application's
19
+ -- export (kept by pgkiln import --replace) but part of the workspace export
20
+ -- (Workspace utilities → Export and import, part "team").
21
+ -- =====================================================================
22
+
23
+ create table meta.team_label_group (
24
+ id bigint generated always as identity primary key,
25
+ workspace_id int not null references meta.workspace on delete cascade,
26
+ name text not null check (name ~ '^\S(.{0,38}\S)?$'),
27
+ description text check (length(description) <= 500),
28
+ seq int not null default 10
29
+ );
30
+ create unique index team_label_group_name on meta.team_label_group (workspace_id, lower(name));
31
+
32
+ create table meta.team_label (
33
+ id bigint generated always as identity primary key,
34
+ workspace_id int not null references meta.workspace on delete cascade,
35
+ group_id bigint references meta.team_label_group on delete set null,
36
+ name text not null check (name ~ '^\S(.{0,38}\S)?$'),
37
+ -- a colour of a fixed palette (builder.css draws them); never free CSS
38
+ color text not null default 'gray' check (color in ('red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'gray')),
39
+ description text check (length(description) <= 500),
40
+ seq int not null default 10
41
+ );
42
+ create unique index team_label_name on meta.team_label (workspace_id, lower(name));
43
+
44
+ create table meta.team_milestone (
45
+ id bigint generated always as identity primary key,
46
+ workspace_id int not null references meta.workspace on delete cascade,
47
+ name text not null check (name ~ '^\S(.{0,38}\S)?$'),
48
+ description text check (length(description) <= 500),
49
+ due_date date,
50
+ status text not null default 'open' check (status in ('open', 'closed')),
51
+ created_at timestamptz not null default now()
52
+ );
53
+ create unique index team_milestone_name on meta.team_milestone (workspace_id, lower(name));
54
+
55
+ create table meta.team_template (
56
+ id bigint generated always as identity primary key,
57
+ workspace_id int not null references meta.workspace on delete cascade,
58
+ kind text not null check (kind in ('issue', 'response')),
59
+ name text not null check (name ~ '^\S(.{0,58}\S)?$'),
60
+ -- issue templates: the new issue's title and labels (by name)
61
+ title text check (length(title) <= 200),
62
+ body text not null default '' check (length(body) <= 20000),
63
+ labels text[] not null default '{}' check (cardinality(labels) <= 20),
64
+ seq int not null default 10
65
+ );
66
+ create unique index team_template_name on meta.team_template (workspace_id, kind, lower(name));
67
+
68
+ create table meta.team_issue (
69
+ id bigint generated always as identity primary key,
70
+ workspace_id int not null references meta.workspace on delete cascade,
71
+ title text not null check (length(btrim(title)) between 1 and 200),
72
+ body text not null default '' check (length(body) <= 20000),
73
+ status text not null default 'open' check (status in ('open', 'closed')),
74
+ milestone_id bigint references meta.team_milestone on delete set null,
75
+ -- the application (of the same workspace) and page the issue is about
76
+ app_id int references meta.app on delete set null,
77
+ page_no int check (page_no >= 0),
78
+ -- where it came from: a user's feedback or a developer comment
79
+ feedback_id bigint references meta.feedback on delete set null,
80
+ dev_comment_id int references meta.dev_comment on delete set null,
81
+ created_by text not null,
82
+ created_at timestamptz not null default now(),
83
+ updated_at timestamptz not null default now(),
84
+ closed_by text,
85
+ closed_at timestamptz
86
+ );
87
+ create index team_issue_ws on meta.team_issue (workspace_id, status, updated_at desc);
88
+ create index team_issue_page on meta.team_issue (app_id, page_no) where status = 'open';
89
+ create index team_issue_feedback on meta.team_issue (feedback_id) where feedback_id is not null;
90
+ create index team_issue_comment_src on meta.team_issue (dev_comment_id) where dev_comment_id is not null;
91
+
92
+ create table meta.team_issue_label (
93
+ issue_id bigint not null references meta.team_issue on delete cascade,
94
+ label_id bigint not null references meta.team_label on delete cascade,
95
+ primary key (issue_id, label_id)
96
+ );
97
+ create index on meta.team_issue_label (label_id);
98
+
99
+ create table meta.team_issue_assignee (
100
+ issue_id bigint not null references meta.team_issue on delete cascade,
101
+ username text not null references meta.developer (username) on delete cascade on update cascade,
102
+ primary key (issue_id, username)
103
+ );
104
+ create index on meta.team_issue_assignee (username);
105
+
106
+ create table meta.team_issue_comment (
107
+ id bigint generated always as identity primary key,
108
+ issue_id bigint not null references meta.team_issue on delete cascade,
109
+ author text not null,
110
+ body text not null check (length(btrim(body)) between 1 and 20000),
111
+ created_at timestamptz not null default now()
112
+ );
113
+ create index on meta.team_issue_comment (issue_id, created_at);
114
+
115
+ -- the history of an issue: who changed what, when
116
+ create table meta.team_issue_event (
117
+ id bigint generated always as identity primary key,
118
+ issue_id bigint not null references meta.team_issue on delete cascade,
119
+ at timestamptz not null default now(),
120
+ username text,
121
+ action text not null check (action ~ '^[a-z_]{1,30}$'),
122
+ detail text check (length(detail) <= 1000)
123
+ );
124
+ create index on meta.team_issue_event (issue_id, at);
125
+
126
+ -- @mentions of the workspace's developers in an issue's body or a comment; shown in the builder until seen
127
+ create table meta.team_mention (
128
+ id bigint generated always as identity primary key,
129
+ issue_id bigint not null references meta.team_issue on delete cascade,
130
+ comment_id bigint references meta.team_issue_comment on delete cascade,
131
+ username text not null references meta.developer (username) on delete cascade on update cascade,
132
+ by_user text,
133
+ created_at timestamptz not null default now(),
134
+ seen_at timestamptz
135
+ );
136
+ create index on meta.team_mention (username) where seen_at is null;
137
+
138
+ -- a developer's saved views of the issue list (the filters as a query string)
139
+ create table meta.team_view (
140
+ id bigint generated always as identity primary key,
141
+ workspace_id int not null references meta.workspace on delete cascade,
142
+ username text not null references meta.developer (username) on delete cascade on update cascade,
143
+ name text not null check (name ~ '^\S(.{0,38}\S)?$'),
144
+ query text not null default '' check (length(query) <= 1000),
145
+ created_at timestamptz not null default now()
146
+ );
147
+ create unique index team_view_name on meta.team_view (workspace_id, username, lower(name));
148
+
149
+ -- the label's group, the issue's milestone and its labels belong to the issue's workspace
150
+ create function meta.team_same_workspace() returns trigger
151
+ language plpgsql set search_path = pg_catalog as $$
152
+ begin
153
+ -- (nested: a record field is only read in the branch of its own table)
154
+ if tg_table_name = 'team_label' then
155
+ if new.group_id is not null
156
+ and (select g.workspace_id from meta.team_label_group g where g.id = new.group_id) is distinct from new.workspace_id then
157
+ raise exception 'the label group belongs to another workspace';
158
+ end if;
159
+ elsif tg_table_name = 'team_issue' then
160
+ if new.milestone_id is not null
161
+ and (select m.workspace_id from meta.team_milestone m where m.id = new.milestone_id) is distinct from new.workspace_id then
162
+ raise exception 'the milestone belongs to another workspace';
163
+ end if;
164
+ elsif tg_table_name = 'team_issue_label' then
165
+ if (select l.workspace_id from meta.team_label l where l.id = new.label_id)
166
+ is distinct from (select i.workspace_id from meta.team_issue i where i.id = new.issue_id) then
167
+ raise exception 'the label belongs to another workspace';
168
+ end if;
169
+ end if;
170
+ return new;
171
+ end $$;
172
+ create trigger team_same_workspace before insert or update on meta.team_label for each row execute function meta.team_same_workspace();
173
+ create trigger team_same_workspace before insert or update on meta.team_issue for each row execute function meta.team_same_workspace();
174
+ create trigger team_same_workspace before insert or update on meta.team_issue_label for each row execute function meta.team_same_workspace();
175
+
176
+ revoke all on meta.team_label_group, meta.team_label, meta.team_milestone, meta.team_template, meta.team_issue, meta.team_issue_label,
177
+ meta.team_issue_assignee, meta.team_issue_comment, meta.team_issue_event, meta.team_mention, meta.team_view from public;
178
+
179
+ comment on table meta.team_issue is 'Team Development (122): issues of a workspace; workspace data (the workspace export), not exported with an application';
180
+ comment on table meta.team_label is 'Team Development (122): labels of a workspace''s issues, in label groups, with a colour of a fixed palette';
181
+ comment on table meta.team_label_group is 'Team Development (122): label groups (e.g. Type, Priority)';
182
+ comment on table meta.team_milestone is 'Team Development (122): milestones of a workspace';
183
+ comment on table meta.team_template is 'Team Development (122): issue templates and canned responses';
184
+ comment on table meta.team_issue_event is 'Team Development (122): the history of an issue';
185
+ comment on table meta.team_mention is 'Team Development (122): @mentions of developers, shown in the builder until seen';
186
+ comment on table meta.team_view is 'Team Development (122): saved views of the issue list, per developer';