pgkiln 0.50.0 → 0.52.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 (104) hide show
  1. package/SECURITY.md +7 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/db/migrations/121_ai_agents.sql +94 -0
  5. package/db/migrations/122_team_development.sql +186 -0
  6. package/db/migrations/123_workflow_outcomes.sql +407 -0
  7. package/db/migrations/124_component_details.sql +51 -0
  8. package/docs/apex-feature-parity.md +15 -12
  9. package/docs/guide/01-installation.md +9 -3
  10. package/docs/guide/03-builder.md +211 -11
  11. package/docs/guide/04-pages-and-regions.md +96 -22
  12. package/docs/guide/05-items.md +7 -1
  13. package/docs/guide/06-processing.md +123 -8
  14. package/docs/guide/07-dynamic-actions.md +34 -0
  15. package/docs/guide/09-reference.md +2 -1
  16. package/docs/guide/11-from-apex.md +2 -1
  17. package/docs/guide/12-development.md +70 -11
  18. package/docs/guide/20-ai-agents.md +4 -2
  19. package/examples/hr/README.md +2 -0
  20. package/examples/hr/hr_63_datasets.sql +29 -0
  21. package/examples/hr/hr_64_ai_agent.sql +97 -0
  22. package/examples/hr/hr_65_ask_reports.sql +21 -0
  23. package/examples/hr/hr_66_team_development.sql +58 -0
  24. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  25. package/examples/hr/hr_68_component_details.sql +96 -0
  26. package/package.json +1 -1
  27. package/public/app.css +46 -0
  28. package/public/app.js +134 -20
  29. package/public/builder.css +116 -0
  30. package/public/builder.js +47 -0
  31. package/src/ai/chat.ts +18 -18
  32. package/src/ai/service.ts +35 -11
  33. package/src/ai/types.ts +7 -2
  34. package/src/app.ts +11 -1
  35. package/src/builder/advisor.ts +6 -3
  36. package/src/builder/ai-agents.ts +372 -0
  37. package/src/builder/ai-builder.ts +3 -3
  38. package/src/builder/ai.ts +13 -9
  39. package/src/builder/assistant.ts +22 -7
  40. package/src/builder/components.ts +38 -9
  41. package/src/builder/datasets.ts +472 -0
  42. package/src/builder/designer.ts +8 -3
  43. package/src/builder/feedback.ts +6 -1
  44. package/src/builder/home.ts +12 -7
  45. package/src/builder/instance.ts +2 -0
  46. package/src/builder/locks.ts +7 -1
  47. package/src/builder/region-help.ts +2 -2
  48. package/src/builder/reporter.ts +52 -3
  49. package/src/builder/routes.ts +7 -4
  50. package/src/builder/shared.ts +1 -1
  51. package/src/builder/subscriptions.ts +1 -0
  52. package/src/builder/team.ts +991 -0
  53. package/src/builder/ui.ts +8 -5
  54. package/src/builder/workflow-migrate.ts +210 -0
  55. package/src/builder/workflows.ts +29 -6
  56. package/src/builder/workspace-home.ts +285 -0
  57. package/src/builder/workspace-transfer.ts +1 -1
  58. package/src/builder/workspaces.ts +7 -4
  59. package/src/cli/replace.ts +7 -1
  60. package/src/datasets.ts +367 -0
  61. package/src/i18n/ar.ts +33 -0
  62. package/src/i18n/cs.ts +33 -0
  63. package/src/i18n/da.ts +33 -0
  64. package/src/i18n/de.ts +33 -0
  65. package/src/i18n/el.ts +33 -0
  66. package/src/i18n/es.ts +33 -0
  67. package/src/i18n/fi.ts +33 -0
  68. package/src/i18n/fr.ts +33 -0
  69. package/src/i18n/he.ts +33 -0
  70. package/src/i18n/it.ts +33 -0
  71. package/src/i18n/ja.ts +33 -0
  72. package/src/i18n/ko.ts +33 -0
  73. package/src/i18n/nb.ts +33 -0
  74. package/src/i18n/pl.ts +33 -0
  75. package/src/i18n/pt.ts +33 -0
  76. package/src/i18n/ru.ts +33 -0
  77. package/src/i18n/sv.ts +33 -0
  78. package/src/i18n/tr.ts +33 -0
  79. package/src/i18n/uk.ts +33 -0
  80. package/src/i18n/zh.ts +33 -0
  81. package/src/i18n.ts +66 -0
  82. package/src/instance.ts +3 -0
  83. package/src/metadata.ts +9 -3
  84. package/src/release-check.ts +139 -0
  85. package/src/reporting-app.ts +101 -0
  86. package/src/runtime/ai-agents.ts +365 -0
  87. package/src/runtime/ai-filter.ts +385 -107
  88. package/src/runtime/assistant.ts +64 -11
  89. package/src/runtime/context.ts +2 -0
  90. package/src/runtime/data-reporter.ts +199 -32
  91. package/src/runtime/ir-phrases.ts +812 -0
  92. package/src/runtime/ir-plan.ts +449 -0
  93. package/src/runtime/items.ts +1 -1
  94. package/src/runtime/regions.ts +6 -2
  95. package/src/runtime/render.ts +1 -1
  96. package/src/runtime/report.ts +1 -1
  97. package/src/runtime/tasks.ts +63 -12
  98. package/src/session.ts +2 -0
  99. package/src/subscriptions.ts +2 -1
  100. package/src/task-events.ts +232 -0
  101. package/src/team-samples.ts +74 -0
  102. package/src/team.ts +342 -0
  103. package/src/workflow.ts +135 -18
  104. package/src/workspace-export.ts +39 -4
package/SECURITY.md CHANGED
@@ -97,6 +97,13 @@ first version (MVP). Every finding below has a regression test in
97
97
  | Files | Uploads are size- and type-checked and stored per session (`meta.temp_files` shows only the session's own files). A posted text value can't point a file item at another file, and a "remove file" value only removes files of the form's own record. Downloads need a checksum bound to user, page, item and record, check page access and read as the app's role (RLS applies). They are sent as attachments with `nosniff` and a sandboxing CSP; only PNG, JPEG, GIF, WebP and PDF are shown inline. |
98
98
  | Data loading and PDFs | The `data_load` process loads as the app's role (grants, RLS, triggers apply) and shows only data errors and `RAISE` messages per row. Report PDFs run the report's own query with the same page, region and row checks as the screen. |
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
+ | 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
+ | 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. |
100
107
 
101
108
  ## Findings of the MVP review and their fixes
102
109
 
@@ -0,0 +1,30 @@
1
+ -- ---------------------------------------------------------------------
2
+ -- 119: the Workspace home (sprint 109, WS-1; APEX: the workspace home
3
+ -- page). The builder opens /builder/home after signing in: tool cards, the
4
+ -- workspace's top applications and users by page views over a reporting
5
+ -- timeframe, a summary of counts, the workspace message and a system
6
+ -- message.
7
+ --
8
+ -- meta.developer.home_timeframe: the reporting timeframe each developer
9
+ -- chose for Top applications and Top users (hour, day or week); null is
10
+ -- the default (day). A developer account is installation data: not
11
+ -- exported with an application or a workspace.
12
+ --
13
+ -- meta.workspace.builder_home: what an old /builder link opens in this
14
+ -- workspace (a workspace preference): null or 'apps' = the App Builder (as
15
+ -- before; the App Builder is also at /builder/apps), 'home' = a redirect to
16
+ -- the Workspace home. Travels in a workspace export's settings.
17
+ --
18
+ -- The "new version available" check is an instance setting
19
+ -- (meta.setting 'release_check' = 'on' / 'off'; else the environment
20
+ -- variable PGKILN_RELEASE_CHECK; off by default): no table needed.
21
+ -- ---------------------------------------------------------------------
22
+ alter table meta.developer
23
+ add column home_timeframe text check (home_timeframe in ('hour', 'day', 'week'));
24
+ comment on column meta.developer.home_timeframe is
25
+ 'The reporting timeframe of the developer''s Workspace home (119): hour, day or week; null = day.';
26
+
27
+ alter table meta.workspace
28
+ add column builder_home text check (builder_home in ('apps', 'home'));
29
+ comment on column meta.workspace.builder_home is
30
+ 'What /builder opens in this workspace (119): null or apps = the App Builder, home = the Workspace home.';
@@ -0,0 +1,87 @@
1
+ -- =====================================================================
2
+ -- 120: Data Reporter datasets (APEX 26.1: Data Reporter → Datasets)
3
+ --
4
+ -- A dataset is a reusable, trusted view of a workspace's data for business
5
+ -- users: tables and views of one schema, joined (the joins proposed from
6
+ -- foreign keys), the columns users may see with labels, format masks and
7
+ -- descriptions, and an optional row filter. Developers define datasets in
8
+ -- the builder (Data Reporter); a Data Reporter region offers a dataset as a
9
+ -- source with {"id": "...", "dataset": "<name>"} in config.sources, and
10
+ -- the runtime builds the dataset's SELECT from the definition (identifiers
11
+ -- quoted, values as literals, operators from a whitelist) and runs reports
12
+ -- on it as the application's database role, so grants and row level
13
+ -- security apply.
14
+ --
15
+ -- tables [{"alias": "t1", "table": "emp"},
16
+ -- {"alias": "t2", "table": "dept",
17
+ -- "join": {"to": "t1", "type": "left", "on": [{"column": "deptno", "ref": "deptno"}]}}]
18
+ -- columns [{"name": "ename", "table": "t1", "column": "ename", "label": "Name",
19
+ -- "format": "FML999G990D00", "description": "..."}]
20
+ -- filters [{"table": "t1", "column": "active", "op": "eq", "value": "true"}]
21
+ --
22
+ -- Per workspace, like the SQL Workshop's saved work. Installation data:
23
+ -- not part of an application's export (the builder's Data Reporter exports
24
+ -- and imports datasets as JSON files). The builder writes it as the owner;
25
+ -- the runtime reads it through its own login, and row level security shows
26
+ -- an isolated workspace's login its own workspace's datasets only.
27
+ -- =====================================================================
28
+ create table meta.dataset (
29
+ id bigint generated always as identity primary key,
30
+ workspace_id int not null default 1 references meta.workspace on delete cascade,
31
+ name text not null check (name ~ '^[a-z][a-z0-9_]{0,39}$'),
32
+ label text not null check (length(btrim(label)) between 1 and 80),
33
+ description text check (length(description) <= 500),
34
+ schema_name text not null check (length(schema_name) between 1 and 63 and schema_name !~* '^(pg_.*|information_schema|meta)$'),
35
+ tables jsonb not null check (jsonb_typeof(tables) = 'array' and jsonb_array_length(tables) between 1 and 10),
36
+ columns jsonb not null default '[]' check (jsonb_typeof(columns) = 'array' and jsonb_array_length(columns) <= 200),
37
+ filters jsonb not null default '[]' check (jsonb_typeof(filters) = 'array' and jsonb_array_length(filters) <= 5),
38
+ created_by text not null,
39
+ created_at timestamptz not null default now(),
40
+ updated_by text,
41
+ updated_at timestamptz not null default now(),
42
+ unique (workspace_id, name)
43
+ );
44
+ revoke all on meta.dataset from public;
45
+ grant select on meta.dataset to pgkiln_runtime;
46
+ comment on table meta.dataset is 'Data Reporter datasets per workspace (120): joined tables and views, offered columns, a row filter; installation data, not exported';
47
+
48
+ -- May this login see a workspace's datasets? An isolated workspace's login: its own only;
49
+ -- other logins: those of workspaces that are not isolated.
50
+ create function meta.dataset_visible(p_workspace int) returns boolean
51
+ language sql stable security definer set search_path = meta, pg_catalog as $$
52
+ select case when meta.login_workspace() is not null then p_workspace = meta.login_workspace()
53
+ else not coalesce((select w.isolated from meta.workspace w where w.id = p_workspace), false) end
54
+ $$;
55
+
56
+ -- The workspace whose datasets an application's Data Reporter offers (refused for an
57
+ -- application the login may not see).
58
+ create function meta.dataset_workspace(p_app_id int) returns int
59
+ language sql stable security definer set search_path = meta, pg_catalog as $$
60
+ select meta.app_workspace(meta.checked_app(p_app_id))
61
+ $$;
62
+
63
+ revoke all on function meta.dataset_visible(int), meta.dataset_workspace(int) from public;
64
+ grant execute on function meta.dataset_visible(int), meta.dataset_workspace(int) to pgkiln_runtime;
65
+
66
+ -- (090) an isolated workspace's logins see their own workspace's datasets only
67
+ alter table meta.dataset enable row level security;
68
+ create policy workspace on meta.dataset
69
+ using ((select meta.isolation_off()) or meta.dataset_visible(workspace_id));
70
+
71
+ -- ---------------------------------------------------------------------
72
+ -- Reporting applications: an application generated by the builder's Data
73
+ -- Reporter on one or more datasets (a Data Reporter region is its engine;
74
+ -- roles viewer, editor and administrator). The type is part of the
75
+ -- definition and travels with the export.
76
+ -- ---------------------------------------------------------------------
77
+ alter table meta.app drop constraint app_app_type_check;
78
+ alter table meta.app add constraint app_app_type_check
79
+ check (app_type in ('standard', 'theme', 'library', 'boilerplate', 'reporting'));
80
+ comment on column meta.app.app_type is 'standard, theme, library (components offered to other apps), boilerplate (a starting point for new apps) or reporting (120: generated on Data Reporter datasets)';
81
+
82
+ -- The number of reporting applications of a workspace (the workspace home's summary).
83
+ create function meta.reporting_app_count(p_workspace int) returns int
84
+ language sql stable security definer set search_path = meta, pg_catalog as $$
85
+ select count(*)::int from meta.app a where a.app_type = 'reporting' and meta.app_workspace(a.id) = p_workspace
86
+ $$;
87
+ revoke all on function meta.reporting_app_count(int) from public;
@@ -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';