Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fd4c2f65d |
@ -311,6 +311,79 @@ message DatasourceResult {
|
||||
bytes meta_json = 3;
|
||||
}
|
||||
|
||||
// --- datatables.* (datatables.DataTables) ---
|
||||
|
||||
message DatatablesGetTableByKeyRequest {
|
||||
string table_key = 1;
|
||||
}
|
||||
|
||||
message DatatablesGetTableByKeyResponse {
|
||||
DataTableInfo table = 1;
|
||||
}
|
||||
|
||||
message DatatablesGetRowRequest {
|
||||
string row_id = 1; // UUID
|
||||
}
|
||||
|
||||
message DatatablesGetRowResponse {
|
||||
DataTableRowInfo row = 1;
|
||||
}
|
||||
|
||||
message DatatablesListRowsRequest {
|
||||
string table_id = 1; // UUID
|
||||
int32 limit = 2;
|
||||
int32 offset = 3;
|
||||
}
|
||||
|
||||
message DatatablesListRowsResponse {
|
||||
repeated DataTableRowInfo rows = 1;
|
||||
}
|
||||
|
||||
message DatatablesCreateRowRequest {
|
||||
string table_id = 1; // UUID
|
||||
bytes data_json = 2; // JSON object
|
||||
}
|
||||
|
||||
message DatatablesCreateRowResponse {
|
||||
DataTableRowInfo row = 1;
|
||||
}
|
||||
|
||||
message DatatablesUpdateRowDataRequest {
|
||||
string row_id = 1; // UUID
|
||||
bytes data_json = 2; // JSON object
|
||||
}
|
||||
|
||||
message DatatablesUpdateRowDataResponse {
|
||||
DataTableRowInfo row = 1;
|
||||
}
|
||||
|
||||
message DatatablesFindRowByFieldRequest {
|
||||
string table_id = 1; // UUID
|
||||
string field = 2;
|
||||
string value = 3;
|
||||
}
|
||||
|
||||
message DatatablesFindRowByFieldResponse {
|
||||
// Unset when no row matches (the guest maps that to nil, nil).
|
||||
DataTableRowInfo row = 1;
|
||||
}
|
||||
|
||||
// DataTableInfo mirrors datatables.Table.
|
||||
message DataTableInfo {
|
||||
string id = 1; // UUID
|
||||
string table_key = 2;
|
||||
string name = 3;
|
||||
int32 row_count = 4;
|
||||
}
|
||||
|
||||
// DataTableRowInfo mirrors datatables.Row (data column as JSON bytes).
|
||||
message DataTableRowInfo {
|
||||
string id = 1; // UUID
|
||||
string table_id = 2; // UUID
|
||||
bytes data_json = 3; // JSON object
|
||||
int32 sort_order = 4;
|
||||
}
|
||||
|
||||
// --- users.* (auth.PublicUsers) ---
|
||||
|
||||
message UsersGetByUsernameRequest {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
46
datatables/datatables.go
Normal file
46
datatables/datatables.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Package datatables gives plugins typed access to site data tables (the Data
|
||||
// Platform's data_tables/data_table_rows), which the per-plugin Postgres role
|
||||
// deliberately cannot reach with raw SQL. The CMS implements this interface
|
||||
// host-side and wires it into CoreServices; row payloads cross the ABI as JSON.
|
||||
package datatables
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// DataTables provides read/write access to site data tables for plugins.
|
||||
type DataTables interface {
|
||||
// GetTableByKey resolves a data table by its stable key (e.g. "playgrounds").
|
||||
GetTableByKey(ctx context.Context, tableKey string) (*Table, error)
|
||||
// GetRow fetches a single row by ID.
|
||||
GetRow(ctx context.Context, rowID uuid.UUID) (*Row, error)
|
||||
// ListRows pages through a table's rows in sort order. limit <= 0 applies
|
||||
// the host default.
|
||||
ListRows(ctx context.Context, tableID uuid.UUID, limit, offset int32) ([]Row, error)
|
||||
// CreateRow appends a row with the given JSON object payload.
|
||||
CreateRow(ctx context.Context, tableID uuid.UUID, dataJSON []byte) (*Row, error)
|
||||
// UpdateRowData replaces a row's JSON payload, preserving its sort order.
|
||||
UpdateRowData(ctx context.Context, rowID uuid.UUID, dataJSON []byte) (*Row, error)
|
||||
// FindRowByField returns the first row whose data->>field equals value, or
|
||||
// (nil, nil) when no row matches.
|
||||
FindRowByField(ctx context.Context, tableID uuid.UUID, field, value string) (*Row, error)
|
||||
}
|
||||
|
||||
// Table is a plugin-facing view of a data table.
|
||||
type Table struct {
|
||||
ID uuid.UUID
|
||||
TableKey string
|
||||
Name string
|
||||
RowCount int32
|
||||
}
|
||||
|
||||
// Row is a plugin-facing view of a data table row. DataJSON is the row's data
|
||||
// column verbatim (a JSON object).
|
||||
type Row struct {
|
||||
ID uuid.UUID
|
||||
TableID uuid.UUID
|
||||
DataJSON []byte
|
||||
SortOrder int32
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/content"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/crypto"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/datasources"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/datatables"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/gating"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/menus"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/settings"
|
||||
@ -27,6 +28,7 @@ type CoreServices struct {
|
||||
Crypto crypto.Crypto
|
||||
Menus menus.Menus
|
||||
Datasources datasources.Datasources
|
||||
DataTables datatables.DataTables
|
||||
PublicUsers auth.PublicUsers
|
||||
Subscriptions subscriptions.Subscriptions
|
||||
|
||||
|
||||
@ -405,6 +405,90 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- datatables ---
|
||||
{
|
||||
name: "datatables_get_table_by_key", wantMethod: "datatables.get_table_by_key",
|
||||
resp: &abiv1.DatatablesGetTableByKeyResponse{Table: &abiv1.DataTableInfo{
|
||||
Id: idTable.String(), TableKey: "playgrounds", Name: "Playgrounds", RowCount: 678,
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.GetTableByKey(ctx, "playgrounds")
|
||||
if err != nil || got.ID != idTable || got.TableKey != "playgrounds" || got.RowCount != 678 {
|
||||
t.Errorf("table = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_get_row", wantMethod: "datatables.get_row",
|
||||
resp: &abiv1.DatatablesGetRowResponse{Row: &abiv1.DataTableRowInfo{
|
||||
Id: idRow.String(), TableId: idTable.String(), DataJson: []byte(`{"name":"Whiteman Park"}`), SortOrder: 3,
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.GetRow(ctx, idRow)
|
||||
if err != nil || got.ID != idRow || got.TableID != idTable || got.SortOrder != 3 {
|
||||
t.Errorf("row = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_list_rows", wantMethod: "datatables.list_rows",
|
||||
resp: &abiv1.DatatablesListRowsResponse{Rows: []*abiv1.DataTableRowInfo{
|
||||
{Id: idRow.String(), TableId: idTable.String(), DataJson: []byte(`{"name":"A"}`)},
|
||||
{Id: idItem.String(), TableId: idTable.String(), DataJson: []byte(`{"name":"B"}`), SortOrder: 1},
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.ListRows(ctx, idTable, 100, 0)
|
||||
if err != nil || len(got) != 2 || got[0].ID != idRow || got[1].SortOrder != 1 {
|
||||
t.Errorf("rows = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_create_row", wantMethod: "datatables.create_row",
|
||||
resp: &abiv1.DatatablesCreateRowResponse{Row: &abiv1.DataTableRowInfo{
|
||||
Id: idRow.String(), TableId: idTable.String(), DataJson: []byte(`{"name":"New"}`),
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.CreateRow(ctx, idTable, []byte(`{"name":"New"}`))
|
||||
if err != nil || got.ID != idRow || string(got.DataJSON) != `{"name":"New"}` {
|
||||
t.Errorf("row = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_update_row_data", wantMethod: "datatables.update_row_data",
|
||||
resp: &abiv1.DatatablesUpdateRowDataResponse{Row: &abiv1.DataTableRowInfo{
|
||||
Id: idRow.String(), TableId: idTable.String(), DataJson: []byte(`{"name":"Updated"}`), SortOrder: 7,
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.UpdateRowData(ctx, idRow, []byte(`{"name":"Updated"}`))
|
||||
if err != nil || got.SortOrder != 7 || string(got.DataJSON) != `{"name":"Updated"}` {
|
||||
t.Errorf("row = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_find_row_by_field", wantMethod: "datatables.find_row_by_field",
|
||||
resp: &abiv1.DatatablesFindRowByFieldResponse{Row: &abiv1.DataTableRowInfo{
|
||||
Id: idRow.String(), TableId: idTable.String(), DataJson: []byte(`{"google_place_id":"abc"}`),
|
||||
}},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.FindRowByField(ctx, idTable, "google_place_id", "abc")
|
||||
if err != nil || got == nil || got.ID != idRow {
|
||||
t.Errorf("row = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "datatables_find_row_by_field_miss", wantMethod: "datatables.find_row_by_field",
|
||||
resp: &abiv1.DatatablesFindRowByFieldResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
got, err := cs.DataTables.FindRowByField(ctx, idTable, "google_place_id", "missing")
|
||||
if err != nil || got != nil {
|
||||
t.Errorf("want nil, nil on miss; row = %+v err = %v", got, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
// --- users ---
|
||||
{
|
||||
name: "users_get_by_username", wantMethod: "users.get_by_username",
|
||||
|
||||
@ -34,6 +34,7 @@ func NewCoreServices(call CallFunc) plugin.CoreServices {
|
||||
Crypto: &cryptoStub{base{family: "crypto", call: call}},
|
||||
Menus: &menusStub{base{family: "menus", call: call}},
|
||||
Datasources: &datasourcesStub{base{family: "datasources", call: call}},
|
||||
DataTables: &datatablesStub{base{family: "datatables", call: call}},
|
||||
PublicUsers: &usersStub{base{family: "users", call: call}},
|
||||
Subscriptions: &subscriptionsStub{base{family: "subscriptions", call: call}},
|
||||
Media: &mediaStub{base{family: "media", call: call}},
|
||||
|
||||
102
plugin/wasmguest/caps/datatables.go
Normal file
102
plugin/wasmguest/caps/datatables.go
Normal file
@ -0,0 +1,102 @@
|
||||
package caps
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/datatables"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// datatablesStub implements datatables.DataTables over datatables.* capability
|
||||
// calls. Row payloads cross as JSON bytes.
|
||||
type datatablesStub struct{ base }
|
||||
|
||||
var _ datatables.DataTables = (*datatablesStub)(nil)
|
||||
|
||||
func (s *datatablesStub) GetTableByKey(ctx context.Context, tableKey string) (*datatables.Table, error) {
|
||||
resp := &abiv1.DatatablesGetTableByKeyResponse{}
|
||||
req := &abiv1.DatatablesGetTableByKeyRequest{TableKey: tableKey}
|
||||
if err := s.invoke(ctx, "get_table_by_key", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dataTableFromProto(resp.GetTable()), nil
|
||||
}
|
||||
|
||||
func (s *datatablesStub) GetRow(ctx context.Context, rowID uuid.UUID) (*datatables.Row, error) {
|
||||
resp := &abiv1.DatatablesGetRowResponse{}
|
||||
req := &abiv1.DatatablesGetRowRequest{RowId: rowID.String()}
|
||||
if err := s.invoke(ctx, "get_row", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dataTableRowFromProto(resp.GetRow()), nil
|
||||
}
|
||||
|
||||
func (s *datatablesStub) ListRows(ctx context.Context, tableID uuid.UUID, limit, offset int32) ([]datatables.Row, error) {
|
||||
resp := &abiv1.DatatablesListRowsResponse{}
|
||||
req := &abiv1.DatatablesListRowsRequest{TableId: tableID.String(), Limit: limit, Offset: offset}
|
||||
if err := s.invoke(ctx, "list_rows", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]datatables.Row, 0, len(resp.GetRows()))
|
||||
for _, r := range resp.GetRows() {
|
||||
if row := dataTableRowFromProto(r); row != nil {
|
||||
rows = append(rows, *row)
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (s *datatablesStub) CreateRow(ctx context.Context, tableID uuid.UUID, dataJSON []byte) (*datatables.Row, error) {
|
||||
resp := &abiv1.DatatablesCreateRowResponse{}
|
||||
req := &abiv1.DatatablesCreateRowRequest{TableId: tableID.String(), DataJson: dataJSON}
|
||||
if err := s.invoke(ctx, "create_row", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dataTableRowFromProto(resp.GetRow()), nil
|
||||
}
|
||||
|
||||
func (s *datatablesStub) UpdateRowData(ctx context.Context, rowID uuid.UUID, dataJSON []byte) (*datatables.Row, error) {
|
||||
resp := &abiv1.DatatablesUpdateRowDataResponse{}
|
||||
req := &abiv1.DatatablesUpdateRowDataRequest{RowId: rowID.String(), DataJson: dataJSON}
|
||||
if err := s.invoke(ctx, "update_row_data", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dataTableRowFromProto(resp.GetRow()), nil
|
||||
}
|
||||
|
||||
func (s *datatablesStub) FindRowByField(ctx context.Context, tableID uuid.UUID, field, value string) (*datatables.Row, error) {
|
||||
resp := &abiv1.DatatablesFindRowByFieldResponse{}
|
||||
req := &abiv1.DatatablesFindRowByFieldRequest{TableId: tableID.String(), Field: field, Value: value}
|
||||
if err := s.invoke(ctx, "find_row_by_field", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dataTableRowFromProto(resp.GetRow()), nil
|
||||
}
|
||||
|
||||
func dataTableFromProto(t *abiv1.DataTableInfo) *datatables.Table {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
id, _ := uuid.Parse(t.GetId())
|
||||
return &datatables.Table{
|
||||
ID: id,
|
||||
TableKey: t.GetTableKey(),
|
||||
Name: t.GetName(),
|
||||
RowCount: t.GetRowCount(),
|
||||
}
|
||||
}
|
||||
|
||||
func dataTableRowFromProto(r *abiv1.DataTableRowInfo) *datatables.Row {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
id, _ := uuid.Parse(r.GetId())
|
||||
tableID, _ := uuid.Parse(r.GetTableId())
|
||||
return &datatables.Row{
|
||||
ID: id,
|
||||
TableID: tableID,
|
||||
DataJSON: r.GetDataJson(),
|
||||
SortOrder: r.GetSortOrder(),
|
||||
}
|
||||
}
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_create_row_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_create_row_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"New"}
|
||||
3
plugin/wasmguest/caps/testdata/golden/datatables_create_row_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/datatables_create_row_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
\
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"New"}
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_miss_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_miss_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaagoogle_place_idmissing
|
||||
0
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_miss_resp.pb
vendored
Normal file
0
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_miss_resp.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaagoogle_place_idabc
|
||||
3
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/datatables_find_row_by_field_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
g
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"google_place_id":"abc"}
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_get_row_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_get_row_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
||||
3
plugin/wasmguest/caps/testdata/golden/datatables_get_row_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/datatables_get_row_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
h
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"Whiteman Park"}
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_get_table_by_key_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_get_table_by_key_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
playgrounds
|
||||
3
plugin/wasmguest/caps/testdata/golden/datatables_get_table_by_key_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/datatables_get_table_by_key_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
C
|
||||
$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaplaygroundsPlaygrounds ¦
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_list_rows_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_list_rows_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaad
|
||||
5
plugin/wasmguest/caps/testdata/golden/datatables_list_rows_resp.pb
vendored
Normal file
5
plugin/wasmguest/caps/testdata/golden/datatables_list_rows_resp.pb
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
Z
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"A"}
|
||||
\
|
||||
$44444444-4444-4444-4444-444444444444$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"B"}
|
||||
2
plugin/wasmguest/caps/testdata/golden/datatables_update_row_data_req.pb
vendored
Normal file
2
plugin/wasmguest/caps/testdata/golden/datatables_update_row_data_req.pb
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb{"name":"Updated"}
|
||||
3
plugin/wasmguest/caps/testdata/golden/datatables_update_row_data_resp.pb
vendored
Normal file
3
plugin/wasmguest/caps/testdata/golden/datatables_update_row_data_resp.pb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
b
|
||||
$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa{"name":"Updated"}
|
||||
Loading…
x
Reference in New Issue
Block a user