pgkiln 0.74.0 → 0.75.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.
- package/db/migrations/142_json_sources.sql +426 -0
- package/docs/apex-feature-parity.md +5 -5
- package/docs/guide/03-builder.md +15 -0
- package/docs/guide/04-pages-and-regions.md +1 -0
- package/docs/guide/12-development.md +11 -0
- package/docs/guide/19-rest-data-sources.md +108 -1
- package/examples/hr/hr_77_json_sources.sql +174 -0
- package/package.json +1 -1
- package/src/app.ts +5 -0
- package/src/builder/document-view.ts +117 -0
- package/src/builder/i18n/de.ts +1 -0
- package/src/builder/i18n/en.ts +1 -0
- package/src/builder/i18n/es.ts +1 -0
- package/src/builder/i18n/fr.ts +1 -0
- package/src/builder/i18n/it.ts +1 -0
- package/src/builder/i18n/ja.ts +1 -0
- package/src/builder/i18n/ko.ts +1 -0
- package/src/builder/i18n/nl.ts +1 -0
- package/src/builder/i18n/pt.ts +1 -0
- package/src/builder/i18n/zh-cn.ts +1 -0
- package/src/builder/i18n/zh-tw.ts +1 -0
- package/src/builder/json-sources.ts +395 -0
- package/src/builder/object-actions.ts +1 -0
- package/src/builder/report-settings.ts +9 -0
- package/src/builder/shared.ts +1 -1
- package/src/builder/table-editor.ts +1 -0
- package/src/builder/websources.ts +6 -3
- package/src/json-sources.ts +365 -0
- package/src/runtime/report.ts +3 -1
|
@@ -15,7 +15,8 @@ Web Credentials and the Invoke API process):
|
|
|
15
15
|
puts values of the response into items; the **`invoke_api` workflow step** does the same on the
|
|
16
16
|
server, with workflow variables ([chapter 6](06-processing.md#invoke-api-steps)).
|
|
17
17
|
|
|
18
|
-
Both are **Shared Components
|
|
18
|
+
Both are **Shared Components**, as are **JSON sources** ([below](#json-sources-and-document-views)): forms,
|
|
19
|
+
reports and grids over the documents in a `jsonb` column of your own tables. The server only calls hosts its administrator allows
|
|
19
20
|
([below](#server-configuration-and-the-allow-list)): out of the box, no outgoing request is made.
|
|
20
21
|
|
|
21
22
|
Try it with the HR example: page 23 "Web services" reads the HR example's own REST module
|
|
@@ -355,6 +356,110 @@ A workflow calls a web service with an **`invoke_api` step** instead: the same k
|
|
|
355
356
|
`variables`, `status_variable` and `response_variable` for the items, `&VAR.` for workflow
|
|
356
357
|
variables and an optional `timeout` ([chapter 6](06-processing.md#invoke-api-steps)).
|
|
357
358
|
|
|
359
|
+
## JSON sources and document views
|
|
360
|
+
|
|
361
|
+
(Sprint 142; APEX: Shared Components → Data Sources → **JSON Sources**, and JSON Relational **Duality Views**.)
|
|
362
|
+
A `jsonb` column is a single text item to a form, and one cell to a report. A **JSON source** describes the
|
|
363
|
+
documents in such a column with a **data profile**, and pgkiln makes updatable views from it, so forms, reports,
|
|
364
|
+
interactive grids, master-detail pages, REST (AutoREST) and MCP read and write the documents' values like columns
|
|
365
|
+
of a table.
|
|
366
|
+
|
|
367
|
+
### A JSON source
|
|
368
|
+
|
|
369
|
+
Shared Components → Data Sources → **JSON Sources** → *Add JSON source*:
|
|
370
|
+
|
|
371
|
+
- **Table and jsonb column**: a column of an ordinary table (`hr.emp.profile`), or the one document column of a
|
|
372
|
+
*table of documents* (`id`, `doc`). The table needs a primary key: the views find their row by it. The list
|
|
373
|
+
shows the json and jsonb columns of the tables your SQL Workshop connection may use (an isolated workspace: its
|
|
374
|
+
own schemas).
|
|
375
|
+
- **View**: the name of the view, made in the table's schema.
|
|
376
|
+
- **The view also adds and deletes rows of the table**: for a table of documents. Off (the default), the view
|
|
377
|
+
only changes the documents of existing rows; an insert or delete through it is refused.
|
|
378
|
+
- **Propose from the data** reads up to 200 documents and proposes the profile: a **column** per path to a value
|
|
379
|
+
(nested objects become `office_city` for `office.city`), typed from the values it found (`integer`, `numeric`,
|
|
380
|
+
`boolean`, `date`, `timestamptz` for ISO date-times, `text`, `jsonb` for mixed or nested values), and an **array**
|
|
381
|
+
per list in the documents: a list of objects gets a column per key, a list of values (`["en", "nl"]`) one column
|
|
382
|
+
for the value itself (an empty path). Names, paths and types stay editable; columns you already named are kept.
|
|
383
|
+
A column can be **read only**.
|
|
384
|
+
|
|
385
|
+
**Create views** (the source's page) makes them in one transaction with your SQL Workshop connection and records
|
|
386
|
+
the statements in the [Change Log](03-builder.md#change-log); *Open in the SQL Worksheet* shows them first. After
|
|
387
|
+
changing the profile, *Make the views again* drops and makes them (a view other objects depend on can't be
|
|
388
|
+
dropped: the error says which). The views are ordinary database objects of your application, like the table:
|
|
389
|
+
put the statements in your own migration scripts for the other environments (the source itself travels with the
|
|
390
|
+
application's export as `json_sources`).
|
|
391
|
+
|
|
392
|
+
For `hr.emp.profile` with `{"title": "Analyst", "since": "1987-04-19", "office": {"city": "Dallas"},
|
|
393
|
+
"skills": [{"name": "SQL", "level": 5}], "badge": {"color": "blue"}}` the HR example's source EMP_PROFILE makes:
|
|
394
|
+
|
|
395
|
+
| View | Columns | One row per |
|
|
396
|
+
| --- | --- | --- |
|
|
397
|
+
| `hr.emp_profile` | `empno`, `title`, `since` (date), `office_city`, `office_floor` (integer), `remote` (boolean) | employee |
|
|
398
|
+
| `hr.emp_profile_skills` | `empno`, `row_key`, `position`, `skill`, `level` (integer) | element of `skills` |
|
|
399
|
+
| `hr.emp_profile_languages` | `empno`, `row_key`, `position`, `language` | element of `languages` |
|
|
400
|
+
|
|
401
|
+
- A value that doesn't read as its column's type (`"floor": "ground"`) shows **NULL** in that column instead of
|
|
402
|
+
failing the whole query (`meta.input_ok`, PostgreSQL's `pg_input_is_valid`).
|
|
403
|
+
- **Writing keeps what the profile doesn't know.** An `UPDATE` of the view writes only the paths whose values
|
|
404
|
+
changed, into the row's document, in one `UPDATE` of the table (`meta.jsonb_patch`): `badge` above, or any key a
|
|
405
|
+
newer version of another program added, stays. Setting a column to NULL removes its key; a path to a nested
|
|
406
|
+
object makes the objects on the way. The key columns are never changed through the view.
|
|
407
|
+
- **Arrays as rows.** An array view has the table's key, `row_key` and `position` (1, 2 …) and the element's
|
|
408
|
+
columns. `row_key` is the element's contents (an md5) and its occurrence among equal elements: it stays the same
|
|
409
|
+
when other elements are added, changed or removed, so deleting two rows of a grid in one save removes the right
|
|
410
|
+
two. An `INSERT` appends an element (the table's key column says to which row), an `UPDATE` patches one
|
|
411
|
+
(unknown keys of the element stay), a `DELETE` removes it. An element that someone changed meanwhile is no
|
|
412
|
+
longer found and the statement fails (*Reload and try again*): nothing is overwritten.
|
|
413
|
+
- **Security.** The views are `security_invoker` and get the table's grants (SELECT, INSERT, UPDATE, DELETE of each
|
|
414
|
+
role that has them on the table). Their INSTEAD OF triggers (`meta.json_source_write`, `meta.json_array_write`)
|
|
415
|
+
are SECURITY INVOKER too: they write the table as the role that writes the view, so its grants and the table's
|
|
416
|
+
row level security apply, exactly as for a direct `UPDATE`.
|
|
417
|
+
|
|
418
|
+
### Pages on a JSON source
|
|
419
|
+
|
|
420
|
+
The views work everywhere a table does: Create Page, a form's or report's source, the interactive grid, AutoREST
|
|
421
|
+
and MCP. **Create page** on the source's page makes a form on the view with a **detail grid per array** (the
|
|
422
|
+
grid's master is the form's key item, `position` read only, `row_key` hidden) and their save processes; open it
|
|
423
|
+
with the key in the URL, for example from a report's link. HR page 51 *Employee profiles* (Administration) is
|
|
424
|
+
such a page with a report on top:
|
|
425
|
+
|
|
426
|
+
```sql
|
|
427
|
+
-- the report's source: hr.emp with its jsonb column
|
|
428
|
+
select empno, ename, job, profile from hr.emp order by profile is null, ename
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### JSON path columns in reports
|
|
432
|
+
|
|
433
|
+
A report region's **JSON path columns** (Page Designer → the report's settings) add columns read from a json or
|
|
434
|
+
jsonb column of its query, without writing the SQL: `name=column:path[:type]`, separated by `;`:
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
title=profile:title; first_skill=profile:skills.0.name; since=profile:since:date
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
A path is keys separated by dots; a number picks an element of an array (`skills.0.name`). They are columns like
|
|
441
|
+
the others: search, filters, sorting, control breaks, aggregates, headings, format masks and downloads. In the
|
|
442
|
+
region's JSON: `"json_columns": [{"name": "title", "column": "profile", "path": "title", "type": "text"}]`.
|
|
443
|
+
|
|
444
|
+
### Document views (duality views)
|
|
445
|
+
|
|
446
|
+
SQL Workshop › Table Properties → **Document view…** (or a table's ⋯ menu) makes a view with the table's key and a
|
|
447
|
+
`jsonb` document per row: the row's columns and, per **child table** you tick (tables whose foreign keys reference
|
|
448
|
+
it), a list of its rows without the foreign key columns:
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
{"deptno": 10, "dname": "ACCOUNTING", "loc": "NEW YORK",
|
|
452
|
+
"emp": [{"empno": 7782, "ename": "CLARK", "job": "MANAGER", "sal": 2450}, …]}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Writing the document back (`update hr.dept_doc set doc = $1 where deptno = 10`, or a PUT through AutoREST on the
|
|
456
|
+
view) updates the row's columns the document names, **upserts** the children it lists (by their key; one without
|
|
457
|
+
a key is inserted) and **deletes** the children it leaves out. A document without the list leaves the children
|
|
458
|
+
alone. An `INSERT` of a document inserts the row and its children, a `DELETE` removes the children and the row.
|
|
459
|
+
Full-text and vector columns are left out of the documents. Like APEX's duality views this gives REST clients and
|
|
460
|
+
MCP tools one document per business object; the trigger (`meta.json_document_write`) is SECURITY INVOKER, so the
|
|
461
|
+
tables' grants and row level security apply to every row it touches.
|
|
462
|
+
|
|
358
463
|
## Export, import and application files
|
|
359
464
|
|
|
360
465
|
The export ([chapter 3](03-builder.md#export-format)) has two new sections: `web_credentials`
|
|
@@ -363,6 +468,8 @@ synchronisation settings, without the synchronisation's next and last run; an im
|
|
|
363
468
|
the schedule off). In the directory format they are
|
|
364
469
|
`shared/web-credentials/` and `shared/rest-sources/` ([chapter 18](18-cli.md)). Regions, lists of
|
|
365
470
|
values, processes and workflow steps refer to sources and credentials by name, so nothing needs remapping.
|
|
471
|
+
JSON sources are part of the application (`json_sources`, in `app.yaml` of the directory format); their
|
|
472
|
+
views are not: they belong to your schema's migration scripts, like the table.
|
|
366
473
|
|
|
367
474
|
## Security notes
|
|
368
475
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 77: a JSON source (sprint 142, AP-8; migration 142;
|
|
3
|
+
-- docs/guide/19-rest-data-sources.md "JSON sources")
|
|
4
|
+
--
|
|
5
|
+
-- hr.emp gets a jsonb column "profile": a document per employee with a
|
|
6
|
+
-- job title, a start date, an office, skills (objects) and languages
|
|
7
|
+
-- (values), and keys no form knows about (e.g. "badge"). JSON source
|
|
8
|
+
-- EMP_PROFILE (Shared Components → JSON sources) describes it, and the
|
|
9
|
+
-- three views below are what its "Create views" makes:
|
|
10
|
+
--
|
|
11
|
+
-- hr.emp_profile empno and a column per path (title, since,
|
|
12
|
+
-- office_city, office_floor, remote);
|
|
13
|
+
-- hr.emp_profile_skills a row per skill: empno, row_key, position,
|
|
14
|
+
-- skill, level;
|
|
15
|
+
-- hr.emp_profile_languages a row per language.
|
|
16
|
+
--
|
|
17
|
+
-- Saving the form changes only the paths whose values changed, so "badge"
|
|
18
|
+
-- and anything else in the document stay. Page 51 "Employee profiles"
|
|
19
|
+
-- (Administration): a report of the employees with JSON path columns
|
|
20
|
+
-- (title and first skill read from the document), a form on
|
|
21
|
+
-- hr.emp_profile and the skills and languages as detail grids.
|
|
22
|
+
-- =====================================================================
|
|
23
|
+
|
|
24
|
+
alter table hr.emp add column if not exists profile jsonb
|
|
25
|
+
check (profile is null or jsonb_typeof(profile) = 'object');
|
|
26
|
+
comment on column hr.emp.profile is 'Profile document (JSON source EMP_PROFILE): title, since, office {city, floor}, remote, skills [{name, level}], languages []';
|
|
27
|
+
|
|
28
|
+
update hr.emp e set profile = p.doc::jsonb
|
|
29
|
+
from (values
|
|
30
|
+
(7839, '{"title": "Chief executive", "since": "1981-11-17", "office": {"city": "New York", "floor": 30}, "remote": false,
|
|
31
|
+
"skills": [{"name": "Strategy", "level": 5}, {"name": "Negotiation", "level": 4}], "languages": ["en", "fr"],
|
|
32
|
+
"badge": {"color": "gold", "printed": "2024-01-08"}}'),
|
|
33
|
+
(7698, '{"title": "Sales manager", "since": "1981-05-01", "office": {"city": "Chicago", "floor": 12}, "remote": false,
|
|
34
|
+
"skills": [{"name": "Sales", "level": 5}, {"name": "Coaching", "level": 3, "certified": true}], "languages": ["en", "es"]}'),
|
|
35
|
+
(7782, '{"title": "Accounting manager", "since": "1981-06-09", "office": {"city": "New York", "floor": 28},
|
|
36
|
+
"skills": [{"name": "Accounting", "level": 5}, {"name": "SQL", "level": 3}], "languages": ["en"]}'),
|
|
37
|
+
(7566, '{"title": "Research manager", "since": "1981-04-02", "office": {"city": "Dallas", "floor": 4}, "remote": true,
|
|
38
|
+
"skills": [{"name": "PostgreSQL", "level": 5}, {"name": "Statistics", "level": 4}], "languages": ["en", "de", "nl"],
|
|
39
|
+
"badge": {"color": "blue"}}'),
|
|
40
|
+
(7788, '{"title": "Analyst", "since": "1987-04-19", "office": {"city": "Dallas"}, "remote": true,
|
|
41
|
+
"skills": [{"name": "SQL", "level": 5}, {"name": "TypeScript", "level": 3}], "languages": ["en"]}')
|
|
42
|
+
) as p (empno, doc)
|
|
43
|
+
where e.empno = p.empno and e.profile is null;
|
|
44
|
+
|
|
45
|
+
-- the JSON source (the builder writes this list; it travels with the application's export)
|
|
46
|
+
update meta.app set json_sources = json_sources || $j$[{"name": "EMP_PROFILE",
|
|
47
|
+
"description": "The employee profile (hr.emp.profile): job title, office, skills and languages",
|
|
48
|
+
"schema": "hr", "table": "emp", "key": ["empno"], "column": "profile", "view": "emp_profile",
|
|
49
|
+
"columns": [{"name": "title", "path": ["title"], "type": "text"}, {"name": "since", "path": ["since"], "type": "date"},
|
|
50
|
+
{"name": "office_city", "path": ["office", "city"], "type": "text"},
|
|
51
|
+
{"name": "office_floor", "path": ["office", "floor"], "type": "integer"},
|
|
52
|
+
{"name": "remote", "path": ["remote"], "type": "boolean"}],
|
|
53
|
+
"arrays": [{"name": "skills", "path": ["skills"], "view": "emp_profile_skills",
|
|
54
|
+
"columns": [{"name": "skill", "path": ["name"], "type": "text"}, {"name": "level", "path": ["level"], "type": "integer"}]},
|
|
55
|
+
{"name": "languages", "path": ["languages"], "view": "emp_profile_languages",
|
|
56
|
+
"columns": [{"name": "language", "path": [], "type": "text"}]}]}]$j$::jsonb
|
|
57
|
+
where alias = 'hr' and not json_sources @> '[{"name": "EMP_PROFILE"}]';
|
|
58
|
+
|
|
59
|
+
-- the views, as Shared Components → JSON sources → Create views makes them
|
|
60
|
+
create view hr.emp_profile with (security_invoker = true) as
|
|
61
|
+
select t.empno,
|
|
62
|
+
t.profile #>> array['title'] as title,
|
|
63
|
+
case when meta.input_ok(t.profile #>> array['since'], 'date') then (t.profile #>> array['since'])::date end as since,
|
|
64
|
+
t.profile #>> array['office', 'city'] as office_city,
|
|
65
|
+
case when meta.input_ok(t.profile #>> array['office', 'floor'], 'integer') then (t.profile #>> array['office', 'floor'])::integer end as office_floor,
|
|
66
|
+
case when meta.input_ok(t.profile #>> array['remote'], 'boolean') then (t.profile #>> array['remote'])::boolean end as remote
|
|
67
|
+
from hr.emp t;
|
|
68
|
+
|
|
69
|
+
comment on view hr.emp_profile is 'JSON source EMP_PROFILE of application hr: hr.emp.profile (made by pgkiln; Shared Components → JSON sources makes it again)';
|
|
70
|
+
|
|
71
|
+
create trigger json_source_write
|
|
72
|
+
instead of insert or update or delete on hr.emp_profile
|
|
73
|
+
for each row execute function meta.json_source_write('{"name":"EMP_PROFILE","schema":"hr","table":"emp","key":["empno"],"column":"profile","rows":false,"columns":[{"name":"title","path":["title"]},{"name":"since","path":["since"]},{"name":"office_city","path":["office","city"]},{"name":"office_floor","path":["office","floor"]},{"name":"remote","path":["remote"]}]}');
|
|
74
|
+
|
|
75
|
+
grant delete, insert, select, update on hr.emp_profile to "hr_app";
|
|
76
|
+
|
|
77
|
+
create view hr.emp_profile_skills with (security_invoker = true) as
|
|
78
|
+
select t.empno,
|
|
79
|
+
k.key as row_key,
|
|
80
|
+
k.idx + 1 as position,
|
|
81
|
+
e.v #>> array['name'] as skill,
|
|
82
|
+
case when meta.input_ok(e.v #>> array['level'], 'integer') then (e.v #>> array['level'])::integer end as level
|
|
83
|
+
from hr.emp t
|
|
84
|
+
cross join lateral meta.jsonb_element_keys(t.profile #> array['skills']) k
|
|
85
|
+
cross join lateral (select (t.profile #> array['skills']) -> k.idx as v) e;
|
|
86
|
+
|
|
87
|
+
comment on view hr.emp_profile_skills is 'JSON source EMP_PROFILE of application hr: the array skills of hr.emp.profile, one row per element (made by pgkiln)';
|
|
88
|
+
|
|
89
|
+
create trigger json_source_write
|
|
90
|
+
instead of insert or update or delete on hr.emp_profile_skills
|
|
91
|
+
for each row execute function meta.json_array_write('{"name":"EMP_PROFILE","schema":"hr","table":"emp","key":["empno"],"column":"profile","array":["skills"],"row_key":"row_key","position":"position","columns":[{"name":"skill","path":["name"]},{"name":"level","path":["level"]}]}');
|
|
92
|
+
|
|
93
|
+
grant delete, insert, select, update on hr.emp_profile_skills to "hr_app";
|
|
94
|
+
|
|
95
|
+
create view hr.emp_profile_languages with (security_invoker = true) as
|
|
96
|
+
select t.empno,
|
|
97
|
+
k.key as row_key,
|
|
98
|
+
k.idx + 1 as position,
|
|
99
|
+
e.v #>> array[]::text[] as language
|
|
100
|
+
from hr.emp t
|
|
101
|
+
cross join lateral meta.jsonb_element_keys(t.profile #> array['languages']) k
|
|
102
|
+
cross join lateral (select (t.profile #> array['languages']) -> k.idx as v) e;
|
|
103
|
+
|
|
104
|
+
comment on view hr.emp_profile_languages is 'JSON source EMP_PROFILE of application hr: the array languages of hr.emp.profile, one row per element (made by pgkiln)';
|
|
105
|
+
|
|
106
|
+
create trigger json_source_write
|
|
107
|
+
instead of insert or update or delete on hr.emp_profile_languages
|
|
108
|
+
for each row execute function meta.json_array_write('{"name":"EMP_PROFILE","schema":"hr","table":"emp","key":["empno"],"column":"profile","array":["languages"],"row_key":"row_key","position":"position","columns":[{"name":"language","path":[]}]}');
|
|
109
|
+
|
|
110
|
+
grant delete, insert, select, update on hr.emp_profile_languages to "hr_app";
|
|
111
|
+
|
|
112
|
+
-- ---------------------------------------------------------------- page 51
|
|
113
|
+
|
|
114
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
115
|
+
select id, 51, 'Employee profiles', 'Employee profiles', 1 from meta.app where alias = 'hr';
|
|
116
|
+
|
|
117
|
+
insert into meta.region (page_id, seq, title, type, columns, source, table_name, pk_column, pk_item, condition, config)
|
|
118
|
+
select p.id, r.seq, r.title, r.type, r.cols, r.source, r.table_name, r.pk_column, r.pk_item, r.condition, r.config::jsonb
|
|
119
|
+
from meta.page p join meta.app a on a.id = p.app_id, (values
|
|
120
|
+
(10, 'Employees', 'report', 12,
|
|
121
|
+
'select empno, ename, job, profile from hr.emp order by profile is null, ename', null, null, null, null,
|
|
122
|
+
'{"hidden": ["profile"], "headings": {"empno": "No.", "ename": "Name", "job": "Job", "title": "Title (profile)", "first_skill": "First skill", "since": "Since"},
|
|
123
|
+
"json_columns": [{"name": "title", "column": "profile", "path": "title", "type": "text"},
|
|
124
|
+
{"name": "first_skill", "column": "profile", "path": "skills.0.name", "type": "text"},
|
|
125
|
+
{"name": "since", "column": "profile", "path": "since", "type": "date"}],
|
|
126
|
+
"link": {"page": 51, "column": "empno", "items": {"P51_EMPNO": "#empno#"}}, "page_size": 5}'),
|
|
127
|
+
(20, 'Profile', 'form', 12, null, 'hr.emp_profile', 'empno', 'P51_EMPNO', ':P51_EMPNO is not null', '{}'),
|
|
128
|
+
(30, 'Skills', 'grid', 8,
|
|
129
|
+
'select empno, row_key, position, skill, level
|
|
130
|
+
from hr.emp_profile_skills
|
|
131
|
+
where empno::text = :P51_EMPNO
|
|
132
|
+
order by position', 'hr.emp_profile_skills', 'row_key', null, ':P51_EMPNO is not null',
|
|
133
|
+
'{"master": {"item": "P51_EMPNO", "column": "empno"}, "readonly": ["position"], "layout": {"hidden": ["empno", "row_key"]},
|
|
134
|
+
"headings": {"position": "#", "skill": "Skill", "level": "Level (1-5)"}, "columns": {"skill": {"required": true}}, "row_actions": {"delete": true}}'),
|
|
135
|
+
(40, 'Languages', 'grid', 4,
|
|
136
|
+
'select empno, row_key, position, language
|
|
137
|
+
from hr.emp_profile_languages
|
|
138
|
+
where empno::text = :P51_EMPNO
|
|
139
|
+
order by position', 'hr.emp_profile_languages', 'row_key', null, ':P51_EMPNO is not null',
|
|
140
|
+
'{"master": {"item": "P51_EMPNO", "column": "empno"}, "readonly": ["position"], "layout": {"hidden": ["empno", "row_key"]},
|
|
141
|
+
"headings": {"position": "#", "language": "Language"}, "row_actions": {"delete": true}}')
|
|
142
|
+
) as r (seq, title, type, cols, source, table_name, pk_column, pk_item, condition, config)
|
|
143
|
+
where a.alias = 'hr' and p.page_no = 51;
|
|
144
|
+
|
|
145
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, source_column)
|
|
146
|
+
select p.id, r.id, i.seq, i.name, i.label, i.type, i.col
|
|
147
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
148
|
+
join meta.region r on r.page_id = p.id and r.title = 'Profile', (values
|
|
149
|
+
(10, 'P51_EMPNO', 'Employee', 'hidden', 'empno'),
|
|
150
|
+
(20, 'P51_TITLE', 'Title', 'text', 'title'),
|
|
151
|
+
(30, 'P51_SINCE', 'Since', 'date', 'since'),
|
|
152
|
+
(40, 'P51_OFFICE_CITY', 'Office city', 'text', 'office_city'),
|
|
153
|
+
(50, 'P51_OFFICE_FLOOR', 'Office floor', 'number', 'office_floor'),
|
|
154
|
+
(60, 'P51_REMOTE', 'Works remotely', 'switch', 'remote')
|
|
155
|
+
) as i (seq, name, label, type, col)
|
|
156
|
+
where a.alias = 'hr' and p.page_no = 51;
|
|
157
|
+
|
|
158
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot)
|
|
159
|
+
select p.id, r.id, 10, 'SAVE', 'Apply Changes', 'submit', true
|
|
160
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
161
|
+
join meta.region r on r.page_id = p.id and r.title = 'Profile'
|
|
162
|
+
where a.alias = 'hr' and p.page_no = 51;
|
|
163
|
+
|
|
164
|
+
insert into meta.process (page_id, region_id, seq, name, type, point)
|
|
165
|
+
select p.id, r.id, r.seq, case r.type when 'form' then 'Process form Profile' else 'Save ' || r.title end,
|
|
166
|
+
case r.type when 'form' then 'form_dml' else 'grid_dml' end, 'submit'
|
|
167
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
168
|
+
join meta.region r on r.page_id = p.id and r.type in ('form', 'grid')
|
|
169
|
+
where a.alias = 'hr' and p.page_no = 51;
|
|
170
|
+
|
|
171
|
+
insert into meta.nav_entry (app_id, parent_id, seq, label, icon, target_page)
|
|
172
|
+
select a.id, n.id, 80, 'Employee profiles', 'id-card', 51
|
|
173
|
+
from meta.app a join meta.nav_entry n on n.app_id = a.id and n.label = 'Administration' and n.parent_id is null
|
|
174
|
+
where a.alias = 'hr';
|
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { ldapRoutes } from './builder/ldap.ts';
|
|
|
27
27
|
import { aiRoutes } from './builder/ai.ts';
|
|
28
28
|
import { aiAgentRoutes } from './builder/ai-agents.ts';
|
|
29
29
|
import { vectorProviderRoutes } from './builder/vector-providers.ts';
|
|
30
|
+
import { jsonSourceRoutes } from './builder/json-sources.ts';
|
|
31
|
+
import { documentViewRoutes } from './builder/document-view.ts';
|
|
30
32
|
import { mailRoutes } from './builder/mail.ts';
|
|
31
33
|
import { backupRoutes } from './builder/backups.ts';
|
|
32
34
|
import { availabilityRoutes } from './builder/availability.ts';
|
|
@@ -252,6 +254,9 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
252
254
|
await app.register(aiAgentRoutes);
|
|
253
255
|
// (sprint 136, AP-1) vector providers: Shared Components and Workspace utilities
|
|
254
256
|
await app.register(vectorProviderRoutes);
|
|
257
|
+
// (sprint 142, AP-8) JSON sources (Shared Components) and document views (Table Properties)
|
|
258
|
+
await app.register(jsonSourceRoutes);
|
|
259
|
+
await app.register(documentViewRoutes);
|
|
255
260
|
await app.register(mailRoutes);
|
|
256
261
|
await app.register(backupRoutes);
|
|
257
262
|
await app.register(availabilityRoutes);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
2
|
+
import { joinStatements, recordChanges } from '../changelog.ts';
|
|
3
|
+
import { html, raw, type Raw } from '../html.ts';
|
|
4
|
+
import { icon } from '../icons.ts';
|
|
5
|
+
import { childTables, documentViewDdl, ident, NAME, primaryKey, tableGrants } from '../json-sources.ts';
|
|
6
|
+
import { clientIp, logActivity } from '../session.ts';
|
|
7
|
+
import { readTable } from '../tabledef.ts';
|
|
8
|
+
import { clearCompletions } from './code-editor.ts';
|
|
9
|
+
import { tr } from './i18n.ts';
|
|
10
|
+
import { SYSTEM_SCHEMAS } from './object-browser.ts';
|
|
11
|
+
import { back, BASE, csrf, developer, flash, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
|
|
12
|
+
import { currentWorkspace, usableSchemas, workshopDb } from './workspaces.ts';
|
|
13
|
+
|
|
14
|
+
// (sprint 142, AP-8; APEX: JSON Relational Duality Views) SQL Workshop › Table Properties › Document view: a view
|
|
15
|
+
// with the table's key and a jsonb document of each row with lists of its child rows (tables whose foreign keys
|
|
16
|
+
// reference it, the developer picks them). The view is security_invoker; its INSTEAD OF trigger
|
|
17
|
+
// (meta.json_document_write, migration 142) writes a document back: the row's columns it names, the children it lists
|
|
18
|
+
// (updated by key, or inserted), and deletes the children it leaves out. REST (AutoREST on the view) and MCP read and
|
|
19
|
+
// write whole documents. Preview, Apply (one transaction with the workshop's connection, in the Change Log) and Open
|
|
20
|
+
// in the worksheet, as Add full-text search. Names come from the catalog.
|
|
21
|
+
|
|
22
|
+
async function page(req: Req, reply: FastifyReply) {
|
|
23
|
+
const s = await developer(req, reply);
|
|
24
|
+
if (!s) return;
|
|
25
|
+
const tx = tr(s);
|
|
26
|
+
const db = await workshopDb(s);
|
|
27
|
+
const isPost = req.method === 'POST';
|
|
28
|
+
const b: Body = (isPost ? req.body : req.query) ?? {};
|
|
29
|
+
const ref = String(b.o ?? '');
|
|
30
|
+
const t = ref ? await readTable(db, ref) : null;
|
|
31
|
+
const schemas = (await usableSchemas(db)).filter((n) => !SYSTEM_SCHEMAS.includes(n));
|
|
32
|
+
if (!t || !schemas.includes(t.schema)) return reply.code(404).send('Not found');
|
|
33
|
+
const table = `${ident(t.schema)}.${ident(t.name)}`;
|
|
34
|
+
const key = await primaryKey(db, t.schema, t.name);
|
|
35
|
+
const children = (await childTables(db, t.schema, t.name)).filter((c) => schemas.includes(c.schema));
|
|
36
|
+
// columns a document leaves out: full-text and vector columns (generated or kept by triggers)
|
|
37
|
+
const omitOf = async (schema: string, name: string) => (await db.query<{ n: string }>(
|
|
38
|
+
`select a.attname as n from pg_attribute a join pg_class c on c.oid = a.attrelid join pg_namespace s on s.oid = c.relnamespace join pg_type ty on ty.oid = a.atttypid
|
|
39
|
+
where s.nspname = $1 and c.relname = $2 and a.attnum > 0 and not a.attisdropped and ty.typname in ('tsvector', 'vector', 'halfvec', 'sparsevec')`, [schema, name])).rows.map((r) => r.n);
|
|
40
|
+
const view = isPost ? String(b.view ?? '').trim() : `${t.name}_doc`.toLowerCase().replace(/[^a-z0-9_]+/g, '_').slice(0, 63);
|
|
41
|
+
const document = isPost ? String(b.document ?? '').trim() : 'doc';
|
|
42
|
+
const picked = new Set<number>();
|
|
43
|
+
if (isPost) children.forEach((c, i) => b[`c.${i}.use`] === 'on' && b[`c.${i}.name`] === `${c.schema}.${c.table}` && picked.add(i));
|
|
44
|
+
else children.forEach((_, i) => picked.add(i));
|
|
45
|
+
|
|
46
|
+
const errors: string[] = [];
|
|
47
|
+
let ddl: string[] = [];
|
|
48
|
+
if (!key.length) errors.push(`${table} has no primary key: a document view finds its rows by it.`);
|
|
49
|
+
if (isPost && !errors.length) {
|
|
50
|
+
for (const n of [view, document]) if (!NAME.test(n)) errors.push(tx('fts.bad_name', { name: n }));
|
|
51
|
+
if (key.includes(document)) errors.push(`The document column may not have the name of a key column (${document}).`);
|
|
52
|
+
if (NAME.test(view) && (await db.one('select 1 as x from pg_class where relnamespace = $1::regnamespace and relname = $2', [ident(t.schema), view]))) errors.push(tx('fts.exists', { name: view }));
|
|
53
|
+
if (!errors.length) {
|
|
54
|
+
const kids = [];
|
|
55
|
+
for (const i of picked) kids.push({ ...children[i], omit: await omitOf(children[i].schema, children[i].table) });
|
|
56
|
+
ddl = documentViewDdl({ schema: t.schema, table: t.name, key, view, document, children: kids, omit: await omitOf(t.schema, t.name), grants: await tableGrants(db, t.schema, t.name) });
|
|
57
|
+
}
|
|
58
|
+
if (ddl.length && b.mode === 'worksheet') {
|
|
59
|
+
s.state.__SQLDRAFT = `${ddl.join(';\n\n')};`;
|
|
60
|
+
flash(s, tx('te.review'));
|
|
61
|
+
return back(reply, s, `${BASE}/sql/worksheet`);
|
|
62
|
+
}
|
|
63
|
+
if (ddl.length && b.mode === 'apply') {
|
|
64
|
+
const c = await db.pool.connect();
|
|
65
|
+
let failed = '';
|
|
66
|
+
try {
|
|
67
|
+
await c.query('begin');
|
|
68
|
+
for (const x of ddl) await c.query(x);
|
|
69
|
+
await c.query('commit');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
failed = (e as Error).message;
|
|
72
|
+
await c.query('rollback').catch(() => {});
|
|
73
|
+
} finally {
|
|
74
|
+
c.release(true);
|
|
75
|
+
}
|
|
76
|
+
await logActivity({ username: s.username, event: 'sql_command', ip: clientIp(req), detail: ddl.join(';\n').slice(0, 2000) });
|
|
77
|
+
clearCompletions();
|
|
78
|
+
if (!failed) {
|
|
79
|
+
await recordChanges({ workspaceId: currentWorkspace(s), author: s.username ?? '?', source: 'table', defaultSchema: t.schema, statements: [joinStatements(ddl)] });
|
|
80
|
+
flash(s, `The document view ${t.schema}.${view} is made.`);
|
|
81
|
+
return back(reply, s, `${BASE}/sql/objects/table?${new URLSearchParams({ o: `${ident(t.schema)}.${ident(view)}` })}`);
|
|
82
|
+
}
|
|
83
|
+
errors.push(`${tx('te.nothing_changed')}: ${failed}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const title = `${tx('docv.add')}: ${table}`;
|
|
88
|
+
const form: Raw = key.length ? html`<form method="post" action="${BASE}/sql/objects/document" class="fts-form">${csrf(s)}
|
|
89
|
+
<input type="hidden" name="o" value="${table}">
|
|
90
|
+
<fieldset class="prop-group"><legend>Child tables in the document</legend>
|
|
91
|
+
${children.length ? html`<div class="table-wrap"><table class="report"><thead><tr><th scope="col">${tx('fts.include')}</th><th scope="col">Table</th><th scope="col">List in the document</th><th scope="col">Foreign key</th></tr></thead><tbody>
|
|
92
|
+
${children.map((c, i) => html`<tr><td class="te-c"><input type="hidden" name="c.${i}.name" value="${c.schema}.${c.table}"><input type="checkbox" id="dv-c${i}" name="c.${i}.use"${picked.has(i) ? raw(' checked') : ''}></td>
|
|
93
|
+
<td><label for="dv-c${i}">${c.schema}.${c.table}</label></td><td><code>${c.name}</code></td><td><code>${c.fk.map(([col, p]) => `${col} → ${p}`).join(', ')}</code></td></tr>`)}
|
|
94
|
+
</tbody></table></div>` : html`<p class="muted">No other table references ${table}: the document is the row alone.</p>`}
|
|
95
|
+
<small class="help">Each child row is an object in its list, without the foreign key columns. Writing a document updates the children it lists by their key, inserts the ones without a key, and deletes the ones it leaves out; a document without the list leaves the children alone.</small></fieldset>
|
|
96
|
+
<div class="form-grid u-mt1">
|
|
97
|
+
<div class="field"><label class="label" for="dv-view">View</label><input id="dv-view" name="view" value="${view}" required autocomplete="off" spellcheck="false" class="code"></div>
|
|
98
|
+
<div class="field"><label class="label" for="dv-doc">Document column</label><input id="dv-doc" name="document" value="${document}" required autocomplete="off" spellcheck="false" class="code"></div>
|
|
99
|
+
</div>
|
|
100
|
+
${ddl.length ? html`<h2>DDL</h2><pre class="source te-ddl" data-code-view="sql" tabindex="0" aria-label="DDL">${ddl.map((x) => `${x};`).join('\n\n')}</pre>` : ''}
|
|
101
|
+
<div class="buttons te-buttons">
|
|
102
|
+
<button class="btn" name="mode" value="preview">${icon('eye')} ${tx('te.preview')}</button>
|
|
103
|
+
<button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
|
|
104
|
+
<button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
|
|
105
|
+
<a class="btn" href="${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}">${tx('common.cancel')}</a>
|
|
106
|
+
</div></form>` : html``;
|
|
107
|
+
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}
|
|
108
|
+
<p class="muted">A document view shows each row of ${table} with its child rows as one JSON document, like an Oracle JSON Relational Duality View. Reports, REST (AutoREST on the view) and MCP read whole documents; an update of the document column writes the row and its children back in one statement. The table's row level security and grants apply (the view is security_invoker and gets the table's grants).</p>
|
|
109
|
+
${errors.length ? html`<div class="alert alert-error" role="alert"><ul>${errors.map((e) => html`<li>${e}</li>`)}</ul></div>` : ''}
|
|
110
|
+
${form}`;
|
|
111
|
+
return send(reply, s, shell(s, title, [['SQL Workshop', `${BASE}/sql`], [tx('ob.title'), `${BASE}/sql/objects`], [tx('te.title_table', { table }), `${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}`], [tx('docv.add')]], main, 'sql'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function documentViewRoutes(app: FastifyInstance) {
|
|
115
|
+
app.get(`${BASE}/sql/objects/document`, page);
|
|
116
|
+
app.post(`${BASE}/sql/objects/document`, page);
|
|
117
|
+
}
|
package/src/builder/i18n/de.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const de: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Volltextsuche zu {table} hinzugefügt: Spalte {column} mit Index {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Vektorspalte hinzufügen",
|
|
1080
|
+
'docv.add': "Dokumentansicht",
|
|
1080
1081
|
'vec.title': "Vektorspalte für {table}",
|
|
1081
1082
|
'vec.intro': "Fügt eine Embedding-Spalte vector(n) hinzu (n: die Dimensionen des Vektoranbieters), einen HNSW-Index für die Distanz und einen Trigger, der eine Zeile neu einbettet, wenn sich ihre Quellspalten ändern. Eine Suchkonfiguration vom Typ vector oder hybrid oder das Retrieve-Werkzeug eines KI-Agenten findet Zeilen dann nach Bedeutung. Die vorhandenen Zeilen werden sofort für ihre Embeddings eingereiht.",
|
|
1082
1083
|
'vec.existing': "Vektorspalten jetzt",
|
package/src/builder/i18n/en.ts
CHANGED
|
@@ -1081,6 +1081,7 @@ export const en = {
|
|
|
1081
1081
|
'fts.added': "Added full-text search to {table}: column {column} with index {index}.",
|
|
1082
1082
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1083
1083
|
'vec.add': "Add vector column",
|
|
1084
|
+
'docv.add': "Document view",
|
|
1084
1085
|
'vec.title': "Vector column on {table}",
|
|
1085
1086
|
'vec.intro': "Adds an embedding column vector(n) (n: the vector provider's dimensions), an HNSW index for the distance, and a trigger that re-embeds a row when its source columns change. A search configuration of type vector or hybrid, or an AI agent's Retrieve tool, then finds rows by meaning. The rows there are now are queued for their embeddings at once.",
|
|
1086
1087
|
'vec.existing': "Vector columns now",
|
package/src/builder/i18n/es.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const es: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Búsqueda de texto completo añadida a {table}: columna {column} con el índice {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Añadir columna vectorial",
|
|
1080
|
+
'docv.add': "Vista de documento",
|
|
1080
1081
|
'vec.title': "Columna vectorial en {table}",
|
|
1081
1082
|
'vec.intro': "Añade una columna de embedding vector(n) (n: las dimensiones del proveedor de vectores), un índice HNSW para la distancia y un disparador que vuelve a calcular el embedding de una fila cuando cambian sus columnas de origen. Una configuración de búsqueda de tipo vector o hybrid, o la herramienta Retrieve de un agente de IA, encuentra entonces las filas por su significado. Las filas que ya existen se ponen en cola enseguida para sus embeddings.",
|
|
1082
1083
|
'vec.existing': "Columnas vectoriales actuales",
|
package/src/builder/i18n/fr.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const fr: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Recherche plein texte ajoutée à {table} : colonne {column} avec l'index {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Ajouter une colonne vectorielle",
|
|
1080
|
+
'docv.add': "Vue document",
|
|
1080
1081
|
'vec.title': "Colonne vectorielle sur {table}",
|
|
1081
1082
|
'vec.intro': "Ajoute une colonne d'embedding vector(n) (n : les dimensions du fournisseur de vecteurs), un index HNSW pour la distance et un déclencheur qui recalcule l'embedding d'une ligne quand ses colonnes sources changent. Une configuration de recherche de type vector ou hybrid, ou l'outil Retrieve d'un agent IA, trouve alors les lignes par leur sens. Les lignes existantes sont mises en file d'attente tout de suite pour leurs embeddings.",
|
|
1082
1083
|
'vec.existing': "Colonnes vectorielles actuelles",
|
package/src/builder/i18n/it.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const it: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Ricerca full-text aggiunta a {table}: colonna {column} con l'indice {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Aggiungi colonna vettoriale",
|
|
1080
|
+
'docv.add': "Vista documento",
|
|
1080
1081
|
'vec.title': "Colonna vettoriale su {table}",
|
|
1081
1082
|
'vec.intro': "Aggiunge una colonna di embedding vector(n) (n: le dimensioni del provider di vettori), un indice HNSW per la distanza e un trigger che ricalcola l'embedding di una riga quando cambiano le sue colonne di origine. Una configurazione di ricerca di tipo vector o hybrid, o lo strumento Retrieve di un agente IA, trova allora le righe per significato. Le righe già presenti vengono messe subito in coda per i loro embedding.",
|
|
1082
1083
|
'vec.existing': "Colonne vettoriali attuali",
|
package/src/builder/i18n/ja.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const ja: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "{table} に全文検索を追加しました:列 {column}、索引 {index}。",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "ベクトル列を追加",
|
|
1080
|
+
'docv.add': "ドキュメント・ビュー",
|
|
1080
1081
|
'vec.title': "{table} のベクトル列",
|
|
1081
1082
|
'vec.intro': "埋め込み列 vector(n)(n はベクトル・プロバイダの次元数)、距離用の HNSW 索引、およびソース列が変わったときに行を再埋め込みするトリガーを追加します。その後、検索タイプ vector または hybrid の検索構成や AI エージェントの Retrieve ツールで、意味によって行を検索できます。既存の行はすぐに埋め込みのキューに入れられます。",
|
|
1082
1083
|
'vec.existing': "現在のベクトル列",
|
package/src/builder/i18n/ko.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const ko: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "{table}에 전체 텍스트 검색을 추가했습니다: 열 {column}, 인덱스 {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "벡터 열 추가",
|
|
1080
|
+
'docv.add': "문서 뷰",
|
|
1080
1081
|
'vec.title': "{table}의 벡터 열",
|
|
1081
1082
|
'vec.intro': "임베딩 열 vector(n)(n: 벡터 공급자의 차원 수), 거리용 HNSW 인덱스, 그리고 소스 열이 바뀌면 행을 다시 임베딩하는 트리거를 추가합니다. 그러면 검색 유형 vector 또는 hybrid의 검색 구성이나 AI 에이전트의 Retrieve 도구가 의미로 행을 찾습니다. 지금 있는 행은 바로 임베딩 대기열에 들어갑니다.",
|
|
1082
1083
|
'vec.existing': "현재 벡터 열",
|
package/src/builder/i18n/nl.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const nl: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Volledige-tekstzoeken toegevoegd aan {table}: kolom {column} met index {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Vectorkolom toevoegen",
|
|
1080
|
+
'docv.add': "Documentview",
|
|
1080
1081
|
'vec.title': "Vectorkolom op {table}",
|
|
1081
1082
|
'vec.intro': "Voegt een embeddingkolom vector(n) toe (n: de dimensies van de vectorprovider), een HNSW-index voor de afstand, en een trigger die een rij opnieuw embedt als de bronkolommen veranderen. Een zoekconfiguratie van het type vector of hybrid, of de Retrieve-tool van een AI-agent, vindt rijen dan op betekenis. De rijen die er nu zijn, komen meteen in de wachtrij voor hun embeddings.",
|
|
1082
1083
|
'vec.existing': "Vectorkolommen nu",
|
package/src/builder/i18n/pt.ts
CHANGED
|
@@ -1077,6 +1077,7 @@ export const pt: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "Pesquisa de texto integral adicionada a {table}: coluna {column} com o índice {index}.",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "Adicionar coluna vetorial",
|
|
1080
|
+
'docv.add': "Vista de documento",
|
|
1080
1081
|
'vec.title': "Coluna vetorial em {table}",
|
|
1081
1082
|
'vec.intro': "Adiciona uma coluna de embedding vector(n) (n: as dimensões do fornecedor de vetores), um índice HNSW para a distância e um gatilho que recalcula o embedding de uma linha quando as colunas de origem mudam. Uma configuração de pesquisa do tipo vector ou hybrid, ou a ferramenta Retrieve de um agente de IA, encontra então as linhas pelo significado. As linhas que já existem entram logo na fila para os seus embeddings.",
|
|
1082
1083
|
'vec.existing': "Colunas vetoriais atuais",
|
|
@@ -1077,6 +1077,7 @@ export const zhCN: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "已为 {table} 添加全文搜索:列 {column},索引 {index}。",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "添加向量列",
|
|
1080
|
+
'docv.add': "文档视图",
|
|
1080
1081
|
'vec.title': "{table} 的向量列",
|
|
1081
1082
|
'vec.intro': "添加一个嵌入列 vector(n)(n:向量提供者的维数)、一个用于该距离的 HNSW 索引,以及在源列变化时重新嵌入该行的触发器。之后,搜索类型为 vector 或 hybrid 的搜索配置,或 AI 代理的 Retrieve 工具,就能按含义找到行。现有的行会立即进入嵌入队列。",
|
|
1082
1083
|
'vec.existing': "当前的向量列",
|
|
@@ -1077,6 +1077,7 @@ export const zhTW: BuilderMessages = {
|
|
|
1077
1077
|
'fts.added': "已為 {table} 新增全文檢索:欄位 {column},索引 {index}。",
|
|
1078
1078
|
// (sprint 136, AP-1) Table Properties › Add vector column
|
|
1079
1079
|
'vec.add': "新增向量欄位",
|
|
1080
|
+
'docv.add': "文件檢視",
|
|
1080
1081
|
'vec.title': "{table} 的向量欄位",
|
|
1081
1082
|
'vec.intro': "新增一個嵌入欄位 vector(n)(n:向量提供者的維度數)、一個用於該距離的 HNSW 索引,以及在來源欄位變更時重新嵌入該列的觸發程序。之後,搜尋類型為 vector 或 hybrid 的搜尋組態,或 AI 代理的 Retrieve 工具,就能依含意找到資料列。現有的資料列會立即進入嵌入佇列。",
|
|
1082
1083
|
'vec.existing': "目前的向量欄位",
|