diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py index a3874ce345..9c5c8d0f87 100644 --- a/config/settings/common_settings.py +++ b/config/settings/common_settings.py @@ -73,6 +73,7 @@ def pipe_delim(pipe_string): 'mathesar.rpc.columns.metadata', 'mathesar.rpc.database_setup', 'mathesar.rpc.databases', + 'mathesar.rpc.records', 'mathesar.rpc.roles', 'mathesar.rpc.schemas', 'mathesar.rpc.servers', diff --git a/db/records/operations/select.py b/db/records/operations/select.py index 4da11f90f5..fb5b7fad62 100644 --- a/db/records/operations/select.py +++ b/db/records/operations/select.py @@ -1,6 +1,8 @@ +import json from sqlalchemy import select from sqlalchemy.sql.functions import count +from db import connection as db_conn from db.columns.base import MathesarColumn from db.tables.utils import get_primary_key_column from db.types.operations.cast import get_column_cast_expression @@ -9,6 +11,48 @@ from db.transforms.operations.apply import apply_transformations_deprecated +def list_records_from_table( + conn, + table_oid, + limit=None, + offset=None, + order=None, + filter=None, + group=None, + search=None, +): + """ + Get records from a table. + + The order definition objects should have the form + {"attnum": , "direction": } + + Only data from which the user is granted `SELECT` is returned. + + Args: + tab_id: The OID of the table whose records we'll get. + limit: The maximum number of rows we'll return. + offset: The number of rows to skip before returning records from + following rows. + order: An array of ordering definition objects. + filter: An array of filter definition objects. + group: An array of group definition objects. + search: An array of search definition objects. + """ + result = db_conn.exec_msar_func( + conn, + 'list_records_from_table', + table_oid, + limit, + offset, + json.dumps(order) if order is not None else None, + json.dumps(filter) if filter is not None else None, + json.dumps(group) if group is not None else None, + json.dumps(search) if search is not None else None, + ).fetchone()[0] + return result + + def get_record(table, engine, id_value): primary_key_column = get_primary_key_column(table) pg_query = select(table).where(primary_key_column == id_value) diff --git a/db/sql/00_msar.sql b/db/sql/00_msar.sql index 666c07a534..1c73adc5db 100644 --- a/db/sql/00_msar.sql +++ b/db/sql/00_msar.sql @@ -3228,3 +3228,227 @@ BEGIN RETURN jsonb_build_array(extracted_table_id, fkey_attnum); END; $f$ LANGUAGE plpgsql; + + +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- +-- DQL FUNCTIONS +-- +-- This set of functions is for getting records from python. +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- Data type formatting functions + + +CREATE OR REPLACE FUNCTION msar.format_data(val date) RETURNS text AS $$ +SELECT to_char(val, 'YYYY-MM-DD AD'); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val time without time zone) RETURNS text AS $$ +SELECT concat(to_char(val, 'HH24:MI'), ':', to_char(date_part('seconds', val), 'FM00.0999999999')); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val time with time zone) RETURNS text AS $$ +SELECT CASE + WHEN date_part('timezone_hour', val) = 0 AND date_part('timezone_minute', val) = 0 + THEN concat( + to_char(date_part('hour', val), 'FM00'), ':', to_char(date_part('minute', val), 'FM00'), + ':', to_char(date_part('seconds', val), 'FM00.0999999999'), 'Z' + ) + ELSE + concat( + to_char(date_part('hour', val), 'FM00'), ':', to_char(date_part('minute', val), 'FM00'), + ':', to_char(date_part('seconds', val), 'FM00.0999999999'), + to_char(date_part('timezone_hour', val), 'S00'), ':', + ltrim(to_char(date_part('timezone_minute', val), '00'), '+- ') + ) +END; +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val timestamp without time zone) RETURNS text AS $$ +SELECT + concat( + to_char(val, 'YYYY-MM-DD"T"HH24:MI'), + ':', to_char(date_part('seconds', val), 'FM00.0999999999'), + to_char(val, ' BC') + ); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val timestamp with time zone) RETURNS text AS $$ +SELECT CASE + WHEN date_part('timezone_hour', val) = 0 AND date_part('timezone_minute', val) = 0 + THEN concat( + to_char(val, 'YYYY-MM-DD"T"HH24:MI'), + ':', to_char(date_part('seconds', val), 'FM00.0999999999'), 'Z', to_char(val, ' BC') + ) + ELSE + concat( + to_char(val, 'YYYY-MM-DD"T"HH24:MI'), + ':', to_char(date_part('seconds', val), 'FM00.0999999999'), + to_char(date_part('timezone_hour', val), 'S00'), + ':', ltrim(to_char(date_part('timezone_minute', val), '00'), '+- '), to_char(val, ' BC') + ) +END; +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val interval) returns text AS $$ +SELECT concat( + to_char(val, 'PFMYYYY"Y"FMMM"M"FMDD"D""T"FMHH24"H"FMMI"M"'), date_part('seconds', val), 'S' +); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.format_data(val anyelement) returns anyelement AS $$ +SELECT val; +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION +msar.sanitize_direction(direction text) RETURNS text AS $$/* +*/ +SELECT CASE lower(direction) + WHEN 'asc' THEN 'ASC' + WHEN 'desc' THEN 'DESC' +END; +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.get_pkey_order(tab_id oid) RETURNS jsonb AS $$ +SELECT jsonb_agg(jsonb_build_object('attnum', attnum, 'direction', 'asc')) +FROM pg_constraint, LATERAL unnest(conkey) attnum +WHERE contype='p' AND conrelid=tab_id AND has_column_privilege(tab_id, attnum, 'SELECT'); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION msar.get_total_order(tab_id oid) RETURNS jsonb AS $$ +WITH orderable_cte AS ( + SELECT attnum + FROM pg_catalog.pg_attribute + INNER JOIN pg_catalog.pg_cast ON atttypid=castsource + INNER JOIN pg_catalog.pg_operator ON casttarget=oprleft + WHERE + attrelid = tab_id + AND attnum > 0 + AND NOT attisdropped + AND castcontext = 'i' + AND oprname = '<' + UNION SELECT attnum + FROM pg_catalog.pg_attribute + INNER JOIN pg_catalog.pg_operator ON atttypid=oprleft + WHERE + attrelid = tab_id + AND attnum > 0 + AND NOT attisdropped + AND oprname = '<' + ORDER BY attnum +) +SELECT COALESCE(jsonb_agg(jsonb_build_object('attnum', attnum, 'direction', 'asc')), '[]'::jsonb) +-- This privilege check is redundant in context, but may be useful for other callers. +FROM orderable_cte +-- This privilege check is redundant in context, but may be useful for other callers. +WHERE has_column_privilege(tab_id, attnum, 'SELECT'); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION +msar.build_order_by_expr(tab_id oid, order_ jsonb) RETURNS text AS $$/* +Build an ORDER BY expression for the given table and order JSON. + +The ORDER BY expression will refer to columns by their attnum. This is designed to work together +with `msar.build_selectable_column_expr`. It will only use the columns to which the user has access. +Finally, this function will append either a primary key, or all columns to the produced ORDER BY so +the resulting ordering is totally defined (i.e., deterministic). + +Args: + tab_id: The OID of the table whose columns we'll order by. +*/ +SELECT 'ORDER BY ' || string_agg(format('%I %s', attnum, msar.sanitize_direction(direction)), ', ') +FROM jsonb_to_recordset( + COALESCE( + COALESCE(order_, '[]'::jsonb) || msar.get_pkey_order(tab_id), + COALESCE(order_, '[]'::jsonb) || msar.get_total_order(tab_id) + ) +) + AS x(attnum smallint, direction text) +WHERE has_column_privilege(tab_id, attnum, 'SELECT'); +$$ LANGUAGE SQL; + + +CREATE OR REPLACE FUNCTION +msar.build_selectable_column_expr(tab_id oid) RETURNS text AS $$/* +Build an SQL select-target expression of only columns to which the user has access. + +Given columns with attnums 2, 3, and 4, and assuming the user has access only to columns 2 and 4, +this function will return an expression of the form: + +column_name AS "2", another_column_name AS "4" + +Args: + tab_id: The OID of the table containing the columns to select. +*/ +SELECT string_agg(format('msar.format_data(%I) AS %I', attname, attnum), ', ') +FROM pg_catalog.pg_attribute +WHERE + attrelid = tab_id + AND attnum > 0 + AND NOT attisdropped + AND has_column_privilege(attrelid, attnum, 'SELECT'); +$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION +msar.list_records_from_table( + tab_id oid, + limit_ integer, + offset_ integer, + order_ jsonb, + filter_ jsonb, + group_ jsonb, + search_ jsonb +) RETURNS jsonb AS $$/* +Get records from a table. Only columns to which the user has access are returned. + +Args: + tab_id: The OID of the table whose records we'll get + limit_: The maximum number of rows we'll return + offset_: The number of rows to skip before returning records from following rows. + order_: An array of ordering definition objects. + filter_: An array of filter definition objects. + group_: An array of group definition objects. + search_: An array of search definition objects. + +The order definition objects should have the form + {"attnum": , "direction": } +*/ +DECLARE + records jsonb; +BEGIN + EXECUTE format( + $q$ + WITH count_cte AS ( + SELECT count(1) AS count FROM %2$I.%3$I + ), results_cte AS ( + SELECT %1$s FROM %2$I.%3$I %6$s LIMIT %4$L OFFSET %5$L + ) + SELECT jsonb_build_object( + 'results', jsonb_agg(row_to_json(results_cte.*)), + 'count', max(count_cte.count) + ) + FROM results_cte, count_cte + $q$, + msar.build_selectable_column_expr(tab_id), + msar.get_relation_schema_name(tab_id), + msar.get_relation_name(tab_id), + limit_, + offset_, + msar.build_order_by_expr(tab_id, order_) + ) INTO records; + RETURN records; +END; +$$ LANGUAGE plpgsql; diff --git a/db/sql/test_00_msar.sql b/db/sql/test_00_msar.sql index b12fda83be..91efd314ab 100644 --- a/db/sql/test_00_msar.sql +++ b/db/sql/test_00_msar.sql @@ -2794,3 +2794,186 @@ BEGIN RETURN NEXT ok(NOT jsonb_path_exists(msar.get_roles(), '$[*] ? (@.name == "foo")')); END; $$ LANGUAGE plpgsql; + + +-- msar.format_data -------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION test_format_data() RETURNS SETOF TEXT AS $$ +BEGIN + RETURN NEXT is(msar.format_data('3 Jan, 2021'::date), '2021-01-03 AD'); + RETURN NEXT is(msar.format_data('3 Jan, 23 BC'::date), '0023-01-03 BC'); + RETURN NEXT is(msar.format_data('1 day'::interval), 'P0Y0M1DT0H0M0S'); + RETURN NEXT is( + msar.format_data('1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval), + 'P1Y2M3DT4H5M6S' + ); + RETURN NEXT is(msar.format_data('1 day 3 hours ago'::interval), 'P0Y0M-1DT-3H0M0S'); + RETURN NEXT is(msar.format_data('1 day -3 hours'::interval), 'P0Y0M1DT-3H0M0S'); + RETURN NEXT is( + msar.format_data('1 year -1 month 3 days 14 hours -10 minutes 30.4 seconds'::interval), + 'P0Y11M3DT13H50M30.4S' + ); + RETURN NEXT is( + msar.format_data('1 year -1 month 3 days 14 hours -10 minutes 30.4 seconds ago'::interval), + 'P0Y-11M-3DT-13H-50M-30.4S' + ); + RETURN NEXT is(msar.format_data('45 hours 70 seconds'::interval), 'P0Y0M0DT45H1M10S'); + RETURN NEXT is( + msar.format_data('5 decades 22 years 14 months 1 week 3 days'::interval), + 'P73Y2M10DT0H0M0S' + ); + RETURN NEXT is(msar.format_data('1 century'::interval), 'P100Y0M0DT0H0M0S'); + RETURN NEXT is(msar.format_data('2 millennia'::interval), 'P2000Y0M0DT0H0M0S'); + RETURN NEXT is(msar.format_data('12:30:45+05:30'::time with time zone), '12:30:45.0+05:30'); + RETURN NEXT is(msar.format_data('12:30:45'::time with time zone), '12:30:45.0Z'); + RETURN NEXT is( + msar.format_data('12:30:45.123456-08'::time with time zone), '12:30:45.123456-08:00' + ); + RETURN NEXT is(msar.format_data('12:30'::time without time zone), '12:30:00.0'); + RETURN NEXT is( + msar.format_data('30 July, 2000 19:15:03.65'::timestamp with time zone), + '2000-07-30T19:15:03.65Z AD' + ); + RETURN NEXT is( + msar.format_data('10000-01-01 00:00:00'::timestamp with time zone), + '10000-01-01T00:00:00.0Z AD' + ); + RETURN NEXT is( + msar.format_data('3 March, 25 BC, 17:30:15+01'::timestamp with time zone), + '0025-03-03T16:30:15.0Z BC' + ); + RETURN NEXT is( + msar.format_data('17654-03-02 01:00:00'::timestamp without time zone), + '17654-03-02T01:00:00.0 AD' + ); +END; +$$ LANGUAGE plpgsql; + +-- msar.list_records_from_table -------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION __setup_list_records_table() RETURNS SETOF TEXT AS $$ +BEGIN + CREATE TABLE atable ( + id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + col1 integer, + col2 varchar, + col3 json, + col4 jsonb, + coltodrop integer + ); + ALTER TABLE atable DROP COLUMN coltodrop; + INSERT INTO atable (col1, col2, col3, col4) VALUES + (5, 'sdflkj', '"s"', '{"a": "val"}'), + (34, 'sdflfflsk', null, '[1, 2, 3, 4]'), + (2, 'abcde', '{"k": 3242348}', 'true'); +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION test_list_records_from_table() RETURNS SETOF TEXT AS $$ +DECLARE + rel_id oid; +BEGIN + PERFORM __setup_list_records_table(); + rel_id := 'atable'::regclass::oid; + RETURN NEXT is( + msar.list_records_from_table(rel_id, null, null, null, null, null, null), + $j${ + "count": 3, + "results": [ + {"1": 1, "2": 5, "3": "sdflkj", "4": "s", "5": {"a": "val"}}, + {"1": 2, "2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]}, + {"1": 3, "2": 2, "3": "abcde", "4": {"k": 3242348}, "5": true} + ] + }$j$ + ); + RETURN NEXT is( + msar.list_records_from_table( + rel_id, 2, null, '[{"attnum": 2, "direction": "desc"}]', null, null, null + ), + $j${ + "count": 3, + "results": [ + {"1": 2, "2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]}, + {"1": 1, "2": 5, "3": "sdflkj", "4": "s", "5": {"a": "val"}} + ] + }$j$ + ); + RETURN NEXT is( + msar.list_records_from_table( + rel_id, null, 1, '[{"attnum": 1, "direction": "desc"}]', null, null, null + ), + $j${ + "count": 3, + "results": [ + {"1": 2, "2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]}, + {"1": 1, "2": 5, "3": "sdflkj", "4": "s", "5": {"a": "val"}} + ] + }$j$ + ); + CREATE ROLE intern_no_pkey; + GRANT USAGE ON SCHEMA msar, __msar TO intern_no_pkey; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA msar, __msar TO intern_no_pkey; + GRANT SELECT (col1, col2, col3, col4) ON TABLE atable TO intern_no_pkey; + SET ROLE intern_no_pkey; + RETURN NEXT is( + msar.list_records_from_table(rel_id, null, null, null, null, null, null), + $j${ + "count": 3, + "results": [ + {"2": 2, "3": "abcde", "4": {"k": 3242348}, "5": true}, + {"2": 5, "3": "sdflkj", "4": "s", "5": {"a": "val"}}, + {"2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]} + ] + }$j$ + ); + RETURN NEXT is( + msar.list_records_from_table( + rel_id, null, null, '[{"attnum": 3, "direction": "desc"}]', null, null, null + ), + $j${ + "count": 3, + "results": [ + {"2": 5, "3": "sdflkj", "4": "s", "5": {"a": "val"}}, + {"2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]}, + {"2": 2, "3": "abcde", "4": {"k": 3242348}, "5": true} + ] + }$j$ + ); +END; +$$ LANGUAGE plpgsql; + + +-- msar.build_order_by_expr ------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION test_build_order_by_expr() RETURNS SETOF TEXT AS $$ +DECLARE + rel_id oid; +BEGIN + PERFORM __setup_list_records_table(); + rel_id := 'atable'::regclass::oid; + RETURN NEXT is(msar.build_order_by_expr(rel_id, null), 'ORDER BY "1" ASC'); + RETURN NEXT is( + msar.build_order_by_expr(rel_id, '[{"attnum": 1, "direction": "desc"}]'), + 'ORDER BY "1" DESC, "1" ASC' + ); + RETURN NEXT is( + msar.build_order_by_expr( + rel_id, '[{"attnum": 3, "direction": "asc"}, {"attnum": 5, "direction": "DESC"}]' + ), + 'ORDER BY "3" ASC, "5" DESC, "1" ASC' + ); + CREATE ROLE intern_no_pkey; + GRANT USAGE ON SCHEMA msar, __msar TO intern_no_pkey; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA msar, __msar TO intern_no_pkey; + GRANT SELECT (col1, col2, col3, col4) ON TABLE atable TO intern_no_pkey; + SET ROLE intern_no_pkey; + RETURN NEXT is( + msar.build_order_by_expr(rel_id, null), 'ORDER BY "2" ASC, "3" ASC, "5" ASC' + ); + SET ROLE NONE; + REVOKE ALL ON TABLE atable FROM intern_no_pkey; + SET ROLE intern_no_pkey; + RETURN NEXT is(msar.build_order_by_expr(rel_id, null), null); +END; +$$ LANGUAGE plpgsql; diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md index 94cb74e5ff..0718f47bfa 100644 --- a/docs/docs/api/rpc.md +++ b/docs/docs/api/rpc.md @@ -174,6 +174,15 @@ To use an RPC function: - UniqueConstraint - CreatableConstraintInfo +## Records + +:::records + options: + members: + - list_ + - OrderBy + - RecordList + ## Explorations ::: explorations diff --git a/mathesar/migrations/0012_merge_20240718_0628.py b/mathesar/migrations/0012_merge_20240718_0628.py new file mode 100644 index 0000000000..d13e1ea05c --- /dev/null +++ b/mathesar/migrations/0012_merge_20240718_0628.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.11 on 2024-07-18 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0011_explorations'), + ('mathesar', '0011_rename_role_configuredrole'), + ] + + operations = [ + ] diff --git a/mathesar/rpc/records.py b/mathesar/rpc/records.py new file mode 100644 index 0000000000..f7e085fa43 --- /dev/null +++ b/mathesar/rpc/records.py @@ -0,0 +1,100 @@ +""" +Classes and functions exposed to the RPC endpoint for managing table records. +""" +from typing import TypedDict, Literal + +from modernrpc.core import rpc_method, REQUEST_KEY +from modernrpc.auth.basic import http_basic_auth_login_required + +from db.records.operations.select import list_records_from_table +from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions +from mathesar.rpc.utils import connect + + +class OrderBy(TypedDict): + """ + An object defining an `ORDER BY` clause. + + Attributes: + attnum: The attnum of the column to order by. + direction: The direction to order by. + """ + attnum: int + direction: Literal["asc", "desc"] + + +class RecordList(TypedDict): + """ + Records from a table, along with some meta data + + The form of the objects in the `results` array is determined by the + underlying records being listed. The keys of each object are the + attnums of the retrieved columns. The values are the value for the + given row, for the given column. + + Attributes: + count: The total number of records in the table. + results: An array of record objects. + group: Information for displaying the records grouped in some way. + preview_data: Information for previewing foreign key values. + """ + count: int + results: list[dict] + group: dict + preview_data: list[dict] + + @classmethod + def from_dict(cls, d): + return cls( + count=d["count"], + results=d["results"], + group=None, + preview_data=[], + ) + + +@rpc_method(name="records.list") +@http_basic_auth_login_required +@handle_rpc_exceptions +def list_( + *, + table_oid: int, + database_id: int, + limit: int = None, + offset: int = None, + order: list[OrderBy] = None, + filter: list[dict] = None, + group: list[dict] = None, + search: list[dict] = None, + **kwargs +) -> RecordList: + """ + List records from a table, and its row count. Exposed as `list`. + + Args: + table_oid: Identity of the table in the user's database. + database_id: The Django id of the database containing the table. + limit: The maximum number of rows we'll return. + offset: The number of rows to skip before returning records from + following rows. + order: An array of ordering definition objects. + filter: An array of filter definition objects. + group: An array of group definition objects. + search: An array of search definition objects. + + Returns: + The requested records, along with some metadata. + """ + user = kwargs.get(REQUEST_KEY).user + with connect(database_id, user) as conn: + record_info = list_records_from_table( + conn, + table_oid, + limit=limit, + offset=offset, + order=order, + filter=filter, + group=group, + search=search, + ) + return RecordList.from_dict(record_info) diff --git a/mathesar/tests/rpc/test_endpoints.py b/mathesar/tests/rpc/test_endpoints.py index 6f1c74e3d7..7b64967ff5 100644 --- a/mathesar/tests/rpc/test_endpoints.py +++ b/mathesar/tests/rpc/test_endpoints.py @@ -16,6 +16,7 @@ from mathesar.rpc import database_setup from mathesar.rpc import databases from mathesar.rpc import explorations +from mathesar.rpc import records from mathesar.rpc import roles from mathesar.rpc import schemas from mathesar.rpc import servers @@ -138,6 +139,11 @@ "databases.list", [user_is_authenticated] ), + ( + records.list_, + "records.list", + [user_is_authenticated] + ), ( explorations.list_, "explorations.list", diff --git a/mathesar/tests/rpc/test_records.py b/mathesar/tests/rpc/test_records.py new file mode 100644 index 0000000000..df0afc407d --- /dev/null +++ b/mathesar/tests/rpc/test_records.py @@ -0,0 +1,60 @@ +""" +This file tests the record RPC functions. + +Fixtures: + rf(pytest-django): Provides mocked `Request` objects. + monkeypatch(pytest): Lets you monkeypatch an object for testing. +""" +from contextlib import contextmanager + +from mathesar.rpc import records +from mathesar.models.users import User + + +def test_records_list(rf, monkeypatch): + username = 'alice' + password = 'pass1234' + table_oid = 23457 + database_id = 2 + request = rf.post('/api/rpc/v0/', data={}) + request.user = User(username=username, password=password) + + @contextmanager + def mock_connect(_database_id, user): + if _database_id == database_id and user.username == username: + try: + yield True + finally: + pass + else: + raise AssertionError('incorrect parameters passed') + + def mock_list_records( + conn, + _table_oid, + limit=None, + offset=None, + order=None, + filter=None, + group=None, + search=None, + ): + if _table_oid != table_oid: + raise AssertionError('incorrect parameters passed') + return { + "count": 50123, + "results": [{"1": "abcde", "2": 12345}, {"1": "fghij", "2": 67890}] + } + + monkeypatch.setattr(records, 'connect', mock_connect) + monkeypatch.setattr(records, 'list_records_from_table', mock_list_records) + expect_records_list = { + "count": 50123, + "results": [{"1": "abcde", "2": 12345}, {"1": "fghij", "2": 67890}], + "group": None, + "preview_data": [] + } + actual_records_list = records.list_( + table_oid=table_oid, database_id=database_id, request=request + ) + assert actual_records_list == expect_records_list diff --git a/mathesar/urls.py b/mathesar/urls.py index a0b438376d..8f80565ad0 100644 --- a/mathesar/urls.py +++ b/mathesar/urls.py @@ -61,7 +61,7 @@ path('i18n/', include('django.conf.urls.i18n')), re_path( r'^db/(?P\w+)/(?P\w+)/', - views.schema_home, + views.schemas, name='schema_home' ), ] diff --git a/mathesar/views.py b/mathesar/views.py index e88b596881..4199ee79ee 100644 --- a/mathesar/views.py +++ b/mathesar/views.py @@ -7,6 +7,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response +from mathesar.rpc.schemas import list_ as schemas_list from mathesar.api.db.permissions.database import DatabaseAccessPolicy from mathesar.api.db.permissions.query import QueryAccessPolicy from mathesar.api.db.permissions.schema import SchemaAccessPolicy @@ -26,14 +27,10 @@ def get_schema_list(request, database): - qs = Schema.objects.filter(database=database) - permission_restricted_qs = SchemaAccessPolicy.scope_queryset(request, qs) - schema_serializer = SchemaSerializer( - permission_restricted_qs, - many=True, - context={'request': request} - ) - return schema_serializer.data + if database is not None: + return schemas_list(request=request, database_id=database.id) + else: + return [] def _get_permissible_db_queryset(request): @@ -180,19 +177,6 @@ def get_current_database(request, connection_id): return current_database -def get_current_schema(request, schema_id, database): - # if there's a schema ID passed in, try to retrieve the schema, or return a 404 error. - if schema_id is not None: - permitted_schemas = SchemaAccessPolicy.scope_queryset(request, Schema.objects.all()) - return get_object_or_404(permitted_schemas, id=schema_id) - else: - try: - # Try to get the first schema in the DB - return Schema.objects.filter(database=database).order_by('id').first() - except Schema.DoesNotExist: - return None - - def render_schema(request, database, schema): # if there's no schema available, redirect to the schemas page. if not schema: @@ -300,16 +284,7 @@ def admin_home(request, **kwargs): @login_required -def schema_home(request, connection_id, schema_id, **kwargs): - database = get_current_database(request, connection_id) - schema = get_current_schema(request, schema_id, database) - return render(request, 'mathesar/index.html', { - 'common_data': get_common_data(request, database, schema) - }) - - -@login_required -def schemas(request, connection_id): +def schemas(request, connection_id, **kwargs): database = get_current_database(request, connection_id) return render(request, 'mathesar/index.html', { 'common_data': get_common_data(request, database, None) diff --git a/mathesar_ui/src/AppTypes.ts b/mathesar_ui/src/AppTypes.ts index 168d51b58d..a779ca063e 100644 --- a/mathesar_ui/src/AppTypes.ts +++ b/mathesar_ui/src/AppTypes.ts @@ -1,5 +1,3 @@ -import type { TreeItem } from '@mathesar-component-library/types'; - /** @deprecated in favor of Connection */ export interface Database { id: number; @@ -16,16 +14,6 @@ export interface DBObjectEntry { description: string | null; } -export interface SchemaEntry extends DBObjectEntry { - has_dependencies: boolean; - num_tables: number; - num_queries: number; -} - -export interface SchemaResponse extends SchemaEntry, TreeItem { - tables: DBObjectEntry[]; -} - export type DbType = string; export interface FilterConfiguration { diff --git a/mathesar_ui/src/api/rest/schemas.ts b/mathesar_ui/src/api/rest/schemas.ts deleted file mode 100644 index 9940035c0b..0000000000 --- a/mathesar_ui/src/api/rest/schemas.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { SchemaEntry, SchemaResponse } from '@mathesar/AppTypes'; - -import type { Connection } from './connections'; -import { - type PaginatedResponse, - deleteAPI, - getAPI, - patchAPI, - postAPI, -} from './utils/requestUtils'; - -function list(connectionId: Connection['id']) { - return getAPI>( - `/api/db/v0/schemas/?connection_id=${connectionId}&limit=500`, - ); -} - -function add(props: { - name: SchemaEntry['name']; - description: SchemaEntry['description']; - connectionId: Connection['id']; -}) { - return postAPI('/api/db/v0/schemas/', { - name: props.name, - description: props.description, - connection_id: props.connectionId, - }); -} - -function update(schema: { - id: SchemaEntry['id']; - name?: SchemaEntry['name']; - description?: SchemaEntry['description']; -}) { - return patchAPI(`/api/db/v0/schemas/${schema.id}/`, { - name: schema.name, - description: schema.description, - }); -} - -function deleteSchema(schemaId: SchemaEntry['id']) { - return deleteAPI(`/api/db/v0/schemas/${schemaId}/`); -} - -export default { - list, - add, - update, - delete: deleteSchema, -}; diff --git a/mathesar_ui/src/api/rest/types/queries.ts b/mathesar_ui/src/api/rest/types/queries.ts index 713f32c96a..6517eef56a 100644 --- a/mathesar_ui/src/api/rest/types/queries.ts +++ b/mathesar_ui/src/api/rest/types/queries.ts @@ -1,7 +1,7 @@ import type { Column } from '@mathesar/api/rest/types/tables/columns'; import type { JpPath } from '@mathesar/api/rest/types/tables/joinable_tables'; import type { PaginatedResponse } from '@mathesar/api/rest/utils/requestUtils'; -import type { SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; export type QueryColumnAlias = string; @@ -182,7 +182,7 @@ export interface QueryResultsResponse { export interface QueryRunResponse extends QueryResultsResponse { query: { - schema: SchemaEntry['id']; + schema: Schema['oid']; base_table: QueryInstance['base_table']; initial_columns: QueryInstanceInitialColumn[]; transformations?: QueryInstanceTransformation[]; diff --git a/mathesar_ui/src/api/rest/users.ts b/mathesar_ui/src/api/rest/users.ts index 1020ccc448..477d881d68 100644 --- a/mathesar_ui/src/api/rest/users.ts +++ b/mathesar_ui/src/api/rest/users.ts @@ -1,4 +1,5 @@ -import type { Database, SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; +import type { Database } from '@mathesar/AppTypes'; import type { Language } from '@mathesar/i18n/languages/utils'; import { @@ -27,7 +28,7 @@ export interface DatabaseRole { export interface SchemaRole { id: number; - schema: SchemaEntry['id']; + schema: Schema['oid']; role: UserRole; } @@ -92,7 +93,7 @@ function deleteDatabaseRole(roleId: DatabaseRole['id']) { function addSchemaRole( userId: User['id'], - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], role: UserRole, ) { return postAPI('/api/ui/v0/schema_roles/', { diff --git a/mathesar_ui/src/api/rpc/index.ts b/mathesar_ui/src/api/rpc/index.ts index 0ed7c027c2..e9a3b3c531 100644 --- a/mathesar_ui/src/api/rpc/index.ts +++ b/mathesar_ui/src/api/rpc/index.ts @@ -3,6 +3,7 @@ import Cookies from 'js-cookie'; import { buildRpcApi } from '@mathesar/packages/json-rpc-client-builder'; import { connections } from './connections'; +import { schemas } from './schemas'; /** Mathesar's JSON-RPC API */ export const api = buildRpcApi({ @@ -10,5 +11,6 @@ export const api = buildRpcApi({ getHeaders: () => ({ 'X-CSRFToken': Cookies.get('csrftoken') }), methodTree: { connections, + schemas, }, }); diff --git a/mathesar_ui/src/api/rpc/schemas.ts b/mathesar_ui/src/api/rpc/schemas.ts new file mode 100644 index 0000000000..486a7d4728 --- /dev/null +++ b/mathesar_ui/src/api/rpc/schemas.ts @@ -0,0 +1,47 @@ +import { rpcMethodTypeContainer } from '@mathesar/packages/json-rpc-client-builder'; + +export interface Schema { + oid: number; + name: string; + description: string; + table_count: number; +} + +export const schemas = { + list: rpcMethodTypeContainer< + { + database_id: number; + }, + Schema[] + >(), + + /** Returns the OID of the newly-created schema */ + add: rpcMethodTypeContainer< + { + database_id: number; + name: string; + description?: string; + }, + number + >(), + + patch: rpcMethodTypeContainer< + { + database_id: number; + schema_oid: number; + patch: { + name?: string; + description?: string; + }; + }, + void + >(), + + delete: rpcMethodTypeContainer< + { + database_id: number; + schema_oid: number; + }, + void + >(), +}; diff --git a/mathesar_ui/src/components/AppHeader.svelte b/mathesar_ui/src/components/AppHeader.svelte index 0fa38e1483..3bc094ebca 100644 --- a/mathesar_ui/src/components/AppHeader.svelte +++ b/mathesar_ui/src/components/AppHeader.svelte @@ -62,7 +62,7 @@ isCreatingNewEmptyTable = true; const tableInfo = await createTable(database, schema, {}); isCreatingNewEmptyTable = false; - router.goto(getTablePageUrl(database.id, schema.id, tableInfo.id), false); + router.goto(getTablePageUrl(database.id, schema.oid, tableInfo.id), false); } @@ -90,14 +90,14 @@ {$_('new_table_from_data_import')} {/if} {$_('open_data_explorer')} diff --git a/mathesar_ui/src/components/SchemaName.svelte b/mathesar_ui/src/components/SchemaName.svelte index e6fa7cf638..d3a0199027 100644 --- a/mathesar_ui/src/components/SchemaName.svelte +++ b/mathesar_ui/src/components/SchemaName.svelte @@ -1,10 +1,10 @@ diff --git a/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte b/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte index 734923fb38..477ec9ee99 100644 --- a/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte +++ b/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte @@ -2,7 +2,8 @@
-

- {$_('count_tables', { values: { count: schema.num_tables } })} -

- -

- {$_('count_explorations', { values: { count: schema.num_queries } })} +

+ {$_('count_tables', { values: { count: schema.table_count } })}

diff --git a/mathesar_ui/src/pages/database/SchemaRow.svelte b/mathesar_ui/src/pages/database/SchemaRow.svelte index 6eb6c24f72..534cec6534 100644 --- a/mathesar_ui/src/pages/database/SchemaRow.svelte +++ b/mathesar_ui/src/pages/database/SchemaRow.svelte @@ -2,7 +2,8 @@ import { createEventDispatcher } from 'svelte'; import { _ } from 'svelte-i18n'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import DropdownMenu from '@mathesar/component-library/dropdown-menu/DropdownMenu.svelte'; import MenuDivider from '@mathesar/component-library/menu/MenuDivider.svelte'; import InfoBox from '@mathesar/components/message-boxes/InfoBox.svelte'; @@ -21,13 +22,13 @@ const dispatch = createEventDispatcher(); export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let canExecuteDDL = true; let isHovered = false; let isFocused = false; - $: href = getSchemaPageUrl(database.id, schema.id); + $: href = getSchemaPageUrl(database.id, schema.oid); $: isDefault = schema.name === 'public'; $: isLocked = schema.name === 'public'; diff --git a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte index c5372c4f36..be676788a6 100644 --- a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte +++ b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte @@ -3,7 +3,8 @@ import { router } from 'tinro'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import LayoutWithHeader from '@mathesar/layouts/LayoutWithHeader.svelte'; import { getSchemaPageUrl } from '@mathesar/routes/urls'; import { currentDbAbstractTypes } from '@mathesar/stores/abstract-types'; @@ -22,7 +23,7 @@ const userProfile = getUserProfileStoreFromContext(); export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let query: QueryInstance; export let shareConsumer: ShareConsumer | undefined = undefined; @@ -51,7 +52,7 @@ $: createQueryRunner(query, $currentDbAbstractTypes.data); function gotoSchemaPage() { - router.goto(getSchemaPageUrl(database.id, schema.id)); + router.goto(getSchemaPageUrl(database.id, schema.oid)); } diff --git a/mathesar_ui/src/pages/exploration/Header.svelte b/mathesar_ui/src/pages/exploration/Header.svelte index 5a96c39484..d3ec06a273 100644 --- a/mathesar_ui/src/pages/exploration/Header.svelte +++ b/mathesar_ui/src/pages/exploration/Header.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import EntityPageHeader from '@mathesar/components/EntityPageHeader.svelte'; import { iconExploration, iconInspector } from '@mathesar/icons'; import { getExplorationEditorPageUrl } from '@mathesar/routes/urls'; @@ -11,7 +12,7 @@ import ShareExplorationDropdown from './ShareExplorationDropdown.svelte'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let query: QueryInstance; export let isInspectorOpen = true; export let canEditMetadata: boolean; @@ -30,7 +31,7 @@ {#if canEditMetadata} {$_('edit_in_data_explorer')} diff --git a/mathesar_ui/src/pages/import/preview/ImportPreviewContent.svelte b/mathesar_ui/src/pages/import/preview/ImportPreviewContent.svelte index b6359e8e2c..838161b297 100644 --- a/mathesar_ui/src/pages/import/preview/ImportPreviewContent.svelte +++ b/mathesar_ui/src/pages/import/preview/ImportPreviewContent.svelte @@ -6,7 +6,8 @@ import type { DataFile } from '@mathesar/api/rest/types/dataFiles'; import type { TableEntry } from '@mathesar/api/rest/types/tables'; import type { Column } from '@mathesar/api/rest/types/tables/columns'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import { Field, FieldLayout, @@ -60,7 +61,7 @@ const cancelationRequest = makeDeleteTableRequest(); export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let table: TableEntry; export let dataFile: DataFile; export let useColumnTypeInference = false; @@ -108,7 +109,7 @@ }) { const tableId = props.table?.id ?? table.id; router.goto( - getImportPreviewPageUrl(database.id, schema.id, tableId, { + getImportPreviewPageUrl(database.id, schema.oid, tableId, { useColumnTypeInference: props.useColumnTypeInference ?? useColumnTypeInference, }), @@ -146,7 +147,7 @@ async function cancel() { const response = await cancelationRequest.run({ database, schema, table }); if (response.isOk) { - router.goto(getSchemaPageUrl(database.id, schema.id), true); + router.goto(getSchemaPageUrl(database.id, schema.oid), true); } else { toast.fromError(response.error); } @@ -159,7 +160,7 @@ import_verified: true, columns: finalizeColumns(columns, columnPropertiesMap), }); - router.goto(getTablePageUrl(database.id, schema.id, table.id), true); + router.goto(getTablePageUrl(database.id, schema.oid, table.id), true); } catch (err) { toast.fromError(err); } diff --git a/mathesar_ui/src/pages/import/preview/ImportPreviewPage.svelte b/mathesar_ui/src/pages/import/preview/ImportPreviewPage.svelte index d67efa353b..775b6d0712 100644 --- a/mathesar_ui/src/pages/import/preview/ImportPreviewPage.svelte +++ b/mathesar_ui/src/pages/import/preview/ImportPreviewPage.svelte @@ -3,7 +3,8 @@ import { router } from 'tinro'; import { dataFilesApi } from '@mathesar/api/rest/dataFiles'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import ErrorBox from '@mathesar/components/message-boxes/ErrorBox.svelte'; import { makeSimplePageTitle } from '@mathesar/pages/pageTitleUtils'; import { getTablePageUrl } from '@mathesar/routes/urls'; @@ -19,12 +20,12 @@ const dataFileFetch = new AsyncStore(dataFilesApi.get); export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let tableId: number; export let useColumnTypeInference = false; function redirectToTablePage() { - router.goto(getTablePageUrl(database.id, schema.id, tableId)); + router.goto(getTablePageUrl(database.id, schema.oid, tableId)); } $: void (async () => { diff --git a/mathesar_ui/src/pages/import/preview/importPreviewPageUtils.ts b/mathesar_ui/src/pages/import/preview/importPreviewPageUtils.ts index 262ae59480..6d7474ca8c 100644 --- a/mathesar_ui/src/pages/import/preview/importPreviewPageUtils.ts +++ b/mathesar_ui/src/pages/import/preview/importPreviewPageUtils.ts @@ -5,7 +5,8 @@ import type { TableEntry, } from '@mathesar/api/rest/types/tables'; import type { Column } from '@mathesar/api/rest/types/tables/columns'; -import type { Database, SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; +import type { Database } from '@mathesar/AppTypes'; import { getCellCap } from '@mathesar/components/cell-fabric/utils'; import { getAbstractTypeForDbType } from '@mathesar/stores/abstract-types'; import type { @@ -51,7 +52,7 @@ export function processColumns( export function makeHeaderUpdateRequest() { interface Props { database: Database; - schema: SchemaEntry; + schema: Schema; table: Pick; dataFile: Pick; firstRowIsHeader: boolean; @@ -73,7 +74,7 @@ export function makeHeaderUpdateRequest() { export function makeDeleteTableRequest() { interface Props { database: Database; - schema: SchemaEntry; + schema: Schema; table: Pick; } return new AsyncStore((props: Props) => diff --git a/mathesar_ui/src/pages/import/upload/ImportUploadPage.svelte b/mathesar_ui/src/pages/import/upload/ImportUploadPage.svelte index 35704c6d47..64c670d4d1 100644 --- a/mathesar_ui/src/pages/import/upload/ImportUploadPage.svelte +++ b/mathesar_ui/src/pages/import/upload/ImportUploadPage.svelte @@ -4,7 +4,8 @@ import { dataFilesApi } from '@mathesar/api/rest/dataFiles'; import type { RequestStatus } from '@mathesar/api/rest/utils/requestUtils'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import Spinner from '@mathesar/component-library/spinner/Spinner.svelte'; import DocsLink from '@mathesar/components/DocsLink.svelte'; import { @@ -37,7 +38,7 @@ import DataFileInput from './DataFileInput.svelte'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; interface UploadMethod { key: 'file' | 'url' | 'clipboard'; @@ -117,7 +118,7 @@ }); const previewPage = getImportPreviewPageUrl( database.id, - schema.id, + schema.oid, table.id, { useColumnTypeInference: $useColumnTypeInference }, ); diff --git a/mathesar_ui/src/pages/schema/CreateEmptyTableButton.svelte b/mathesar_ui/src/pages/schema/CreateEmptyTableButton.svelte index 1d95dcea92..6e288c5312 100644 --- a/mathesar_ui/src/pages/schema/CreateEmptyTableButton.svelte +++ b/mathesar_ui/src/pages/schema/CreateEmptyTableButton.svelte @@ -1,13 +1,14 @@ diff --git a/mathesar_ui/src/pages/schema/CreateNewExplorationTutorial.svelte b/mathesar_ui/src/pages/schema/CreateNewExplorationTutorial.svelte index 9592e2677a..ea2eebf2f6 100644 --- a/mathesar_ui/src/pages/schema/CreateNewExplorationTutorial.svelte +++ b/mathesar_ui/src/pages/schema/CreateNewExplorationTutorial.svelte @@ -1,12 +1,13 @@ @@ -18,7 +19,7 @@ {$_('open_data_explorer')} diff --git a/mathesar_ui/src/pages/schema/CreateNewTableButton.svelte b/mathesar_ui/src/pages/schema/CreateNewTableButton.svelte index 2bb296e493..9a22b3733e 100644 --- a/mathesar_ui/src/pages/schema/CreateNewTableButton.svelte +++ b/mathesar_ui/src/pages/schema/CreateNewTableButton.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import { router } from 'tinro'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import Icon from '@mathesar/component-library/icon/Icon.svelte'; import LinkMenuItem from '@mathesar/component-library/menu/LinkMenuItem.svelte'; import { iconAddNew } from '@mathesar/icons'; @@ -15,7 +16,7 @@ } from '@mathesar-component-library'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; let isCreatingNewTable = false; @@ -23,7 +24,7 @@ isCreatingNewTable = true; const tableInfo = await createTable(database, schema, {}); isCreatingNewTable = false; - router.goto(getTablePageUrl(database.id, schema.id, tableInfo.id), false); + router.goto(getTablePageUrl(database.id, schema.oid, tableInfo.id), false); } @@ -44,7 +45,7 @@ {$_('from_scratch')} - + {$_('from_data_import')} diff --git a/mathesar_ui/src/pages/schema/CreateNewTableTutorial.svelte b/mathesar_ui/src/pages/schema/CreateNewTableTutorial.svelte index 230dcfd739..241bb17bb9 100644 --- a/mathesar_ui/src/pages/schema/CreateNewTableTutorial.svelte +++ b/mathesar_ui/src/pages/schema/CreateNewTableTutorial.svelte @@ -1,14 +1,15 @@ @@ -22,7 +23,7 @@ {$_('from_scratch')} - + {$_('import_from_file')} diff --git a/mathesar_ui/src/pages/schema/ExplorationItem.svelte b/mathesar_ui/src/pages/schema/ExplorationItem.svelte index ade361c335..dc2a346478 100644 --- a/mathesar_ui/src/pages/schema/ExplorationItem.svelte +++ b/mathesar_ui/src/pages/schema/ExplorationItem.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import TableName from '@mathesar/components/TableName.svelte'; import { iconExploration } from '@mathesar/icons'; import { getExplorationPageUrl } from '@mathesar/routes/urls'; @@ -11,14 +12,14 @@ export let exploration: QueryInstance; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; $: baseTable = $tablesStore.data.get(exploration.base_table);
diff --git a/mathesar_ui/src/pages/schema/ExplorationsList.svelte b/mathesar_ui/src/pages/schema/ExplorationsList.svelte index 6dd223f709..8fa27682a5 100644 --- a/mathesar_ui/src/pages/schema/ExplorationsList.svelte +++ b/mathesar_ui/src/pages/schema/ExplorationsList.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import { iconExploration } from '@mathesar/icons'; import EmptyEntity from './EmptyEntity.svelte'; @@ -10,7 +11,7 @@ export let explorations: QueryInstance[]; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let bordered = true; diff --git a/mathesar_ui/src/pages/schema/SchemaAccessControlModal.svelte b/mathesar_ui/src/pages/schema/SchemaAccessControlModal.svelte index cb22a2a557..b4dc77cf9c 100644 --- a/mathesar_ui/src/pages/schema/SchemaAccessControlModal.svelte +++ b/mathesar_ui/src/pages/schema/SchemaAccessControlModal.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import type { UserRole } from '@mathesar/api/rest/users'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import Identifier from '@mathesar/components/Identifier.svelte'; import ErrorBox from '@mathesar/components/message-boxes/ErrorBox.svelte'; import { RichText } from '@mathesar/components/rich-text'; @@ -19,7 +20,7 @@ export let controller: ModalController; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; const usersStore = setUsersStoreInContext(); const { requestStatus } = usersStore; diff --git a/mathesar_ui/src/pages/schema/SchemaExplorations.svelte b/mathesar_ui/src/pages/schema/SchemaExplorations.svelte index 4cfee93bce..bf947784f2 100644 --- a/mathesar_ui/src/pages/schema/SchemaExplorations.svelte +++ b/mathesar_ui/src/pages/schema/SchemaExplorations.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import EntityContainerWithFilterBar from '@mathesar/components/EntityContainerWithFilterBar.svelte'; import { RichText } from '@mathesar/components/rich-text'; import { getDataExplorerPageUrl } from '@mathesar/routes/urls'; @@ -12,7 +13,7 @@ import ExplorationsList from './ExplorationsList.svelte'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let explorationsMap: Map; export let hasTablesToExplore: boolean; export let canEditMetadata: boolean; @@ -47,7 +48,7 @@ on:clear={clearQuery} > - + {$_('open_data_explorer')} diff --git a/mathesar_ui/src/pages/schema/SchemaOverview.svelte b/mathesar_ui/src/pages/schema/SchemaOverview.svelte index 4385fc2de7..ad9d8cd6ca 100644 --- a/mathesar_ui/src/pages/schema/SchemaOverview.svelte +++ b/mathesar_ui/src/pages/schema/SchemaOverview.svelte @@ -4,7 +4,8 @@ import type { QueryInstance } from '@mathesar/api/rest/types/queries'; import type { TableEntry } from '@mathesar/api/rest/types/tables'; import type { RequestStatus } from '@mathesar/api/rest/utils/requestUtils'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import SpinnerButton from '@mathesar/component-library/spinner-button/SpinnerButton.svelte'; import ErrorBox from '@mathesar/components/message-boxes/ErrorBox.svelte'; import { iconRefresh } from '@mathesar/icons'; @@ -31,7 +32,7 @@ export let canEditMetadata: boolean; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; $: hasTables = tablesMap.size > 0; $: hasExplorations = explorationsMap.size > 0; @@ -53,14 +54,14 @@ {#if tablesRequestStatus.state === 'processing'} - + {:else if tablesRequestStatus.state === 'failure'}

{tablesRequestStatus.errors[0]}

{ - await refetchTablesForSchema(schema.id); + await refetchTablesForSchema(schema.oid); }} label={$_('retry')} icon={iconRefresh} @@ -94,7 +95,7 @@
{ - await refetchQueriesForSchema(schema.id); + await refetchQueriesForSchema(schema.oid); }} label={$_('retry')} icon={iconRefresh} @@ -125,7 +126,7 @@ {$_('what_is_an_exploration_mini')}
- + {$_('open_data_explorer')}
diff --git a/mathesar_ui/src/pages/schema/SchemaPage.svelte b/mathesar_ui/src/pages/schema/SchemaPage.svelte index fab522012b..e7bc1d4149 100644 --- a/mathesar_ui/src/pages/schema/SchemaPage.svelte +++ b/mathesar_ui/src/pages/schema/SchemaPage.svelte @@ -1,7 +1,8 @@ diff --git a/mathesar_ui/src/routes/DataExplorerRoute.svelte b/mathesar_ui/src/routes/DataExplorerRoute.svelte index 0bbd7b7f67..6e792991fd 100644 --- a/mathesar_ui/src/routes/DataExplorerRoute.svelte +++ b/mathesar_ui/src/routes/DataExplorerRoute.svelte @@ -4,7 +4,8 @@ import { router } from 'tinro'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import type { CancellablePromise } from '@mathesar/component-library'; import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import { iconEdit, iconExploration } from '@mathesar/icons'; @@ -24,7 +25,7 @@ } from '@mathesar/systems/data-explorer'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let queryId: number | undefined; let is404 = false; @@ -42,7 +43,7 @@ try { const url = getExplorationEditorPageUrl( database.id, - schema.id, + schema.oid, instance.id, ); router.goto(url, true); @@ -134,7 +135,7 @@ import { _ } from 'svelte-i18n'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import ErrorPage from '@mathesar/pages/ErrorPage.svelte'; import ExplorationPage from '@mathesar/pages/exploration/ExplorationPage.svelte'; import { queries } from '@mathesar/stores/queries'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let queryId: number; $: query = $queries.data.get(queryId); diff --git a/mathesar_ui/src/routes/ImportRoute.svelte b/mathesar_ui/src/routes/ImportRoute.svelte index 3690eaa72e..7d92797891 100644 --- a/mathesar_ui/src/routes/ImportRoute.svelte +++ b/mathesar_ui/src/routes/ImportRoute.svelte @@ -2,7 +2,8 @@ import { _ } from 'svelte-i18n'; import { Route } from 'tinro'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import { iconImportData } from '@mathesar/icons'; import ImportPreviewPage from '@mathesar/pages/import/preview/ImportPreviewPage.svelte'; @@ -11,13 +12,13 @@ import { getImportPageUrl, getImportPreviewPageQueryParams } from './urls'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; import type { TableEntry } from '@mathesar/api/rest/types/tables'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import RecordPage from '@mathesar/pages/record/RecordPage.svelte'; import RecordStore from '@mathesar/pages/record/RecordStore'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let table: TableEntry; export let recordPk: string; diff --git a/mathesar_ui/src/routes/TableRoute.svelte b/mathesar_ui/src/routes/TableRoute.svelte index 21359c159b..f75cc14cb3 100644 --- a/mathesar_ui/src/routes/TableRoute.svelte +++ b/mathesar_ui/src/routes/TableRoute.svelte @@ -3,7 +3,8 @@ import { _ } from 'svelte-i18n'; import { Route } from 'tinro'; - import type { Database, SchemaEntry } from '@mathesar/AppTypes'; + import type { Schema } from '@mathesar/api/rpc/schemas'; + import type { Database } from '@mathesar/AppTypes'; import AppendBreadcrumb from '@mathesar/components/breadcrumb/AppendBreadcrumb.svelte'; import ErrorPage from '@mathesar/pages/ErrorPage.svelte'; import TablePage from '@mathesar/pages/table/TablePage.svelte'; @@ -12,7 +13,7 @@ import RecordPageRoute from './RecordPageRoute.svelte'; export let database: Database; - export let schema: SchemaEntry; + export let schema: Schema; export let tableId: number; $: $currentTableId = tableId; diff --git a/mathesar_ui/src/stores/queries.ts b/mathesar_ui/src/stores/queries.ts index 1aa0ed73d5..5dc21f936c 100644 --- a/mathesar_ui/src/stores/queries.ts +++ b/mathesar_ui/src/stores/queries.ts @@ -53,32 +53,32 @@ import { postAPI, putAPI, } from '@mathesar/api/rest/utils/requestUtils'; -import type { SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; import CacheManager from '@mathesar/utils/CacheManager'; import { preloadCommonData } from '@mathesar/utils/preloadData'; import { SHARED_LINK_UUID_QUERY_PARAM } from '@mathesar/utils/shares'; import { CancellablePromise } from '@mathesar-component-library'; -import { addCountToSchemaNumExplorations, currentSchemaId } from './schemas'; +import { currentSchemaId } from './schemas'; const commonData = preloadCommonData(); export type UnsavedQueryInstance = Partial; export interface QueriesStoreSubstance { - schemaId: SchemaEntry['id']; + schemaId: Schema['oid']; requestStatus: RequestStatus; data: Map; } // Cache the query list of the last 3 opened schemas const schemasCacheManager = new CacheManager< - SchemaEntry['id'], + Schema['oid'], Writable >(3); const requestMap: Map< - SchemaEntry['id'], + Schema['oid'], CancellablePromise> > = new Map(); @@ -87,7 +87,7 @@ function sortedQueryEntries(queryEntries: QueryInstance[]): QueryInstance[] { } function setSchemaQueriesStore( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], queryEntries?: QueryInstance[], ): Writable { const queries: QueriesStoreSubstance['data'] = new Map(); @@ -120,7 +120,7 @@ function findSchemaStoreForQuery(id: QueryInstance['id']) { } export async function refetchQueriesForSchema( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ): Promise { const store = schemasCacheManager.get(schemaId); if (!store) { @@ -164,7 +164,7 @@ export async function refetchQueriesForSchema( let preload = true; export function getQueriesStoreForSchema( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ): Writable { let store = schemasCacheManager.get(schemaId); if (!store) { @@ -212,7 +212,6 @@ export function createQuery( ): CancellablePromise { const promise = postAPI('/api/db/v0/queries/', newQuery); void promise.then((instance) => { - addCountToSchemaNumExplorations(instance.schema, 1); void refetchQueriesForSchema(instance.schema); return instance; }); @@ -306,7 +305,6 @@ export function deleteQuery(queryId: number): CancellablePromise { storeData.data.delete(queryId); return { ...storeData, data: new Map(storeData.data) }; }); - addCountToSchemaNumExplorations(get(store).schemaId, -1); } return undefined; }); diff --git a/mathesar_ui/src/stores/schemas.ts b/mathesar_ui/src/stores/schemas.ts index 774cd840f1..2a5d31374a 100644 --- a/mathesar_ui/src/stores/schemas.ts +++ b/mathesar_ui/src/stores/schemas.ts @@ -2,12 +2,9 @@ import type { Readable, Unsubscriber, Writable } from 'svelte/store'; import { derived, get, writable } from 'svelte/store'; import type { Connection } from '@mathesar/api/rest/connections'; -import schemasApi from '@mathesar/api/rest/schemas'; -import type { - PaginatedResponse, - RequestStatus, -} from '@mathesar/api/rest/utils/requestUtils'; -import type { SchemaEntry, SchemaResponse } from '@mathesar/AppTypes'; +import type { RequestStatus } from '@mathesar/api/rest/utils/requestUtils'; +import { api } from '@mathesar/api/rpc'; +import type { Schema } from '@mathesar/api/rpc/schemas'; import { preloadCommonData } from '@mathesar/utils/preloadData'; import type { CancellablePromise } from '@mathesar-component-library'; @@ -15,12 +12,13 @@ import { connectionsStore } from './databases'; const commonData = preloadCommonData(); -export const currentSchemaId: Writable = - writable(commonData.current_schema ?? undefined); +export const currentSchemaId: Writable = writable( + commonData.current_schema ?? undefined, +); export interface DBSchemaStoreData { requestStatus: RequestStatus; - data: Map; + data: Map; } const dbSchemaStoreMap: Map< @@ -29,22 +27,16 @@ const dbSchemaStoreMap: Map< > = new Map(); const dbSchemasRequestMap: Map< Connection['id'], - CancellablePromise | undefined> + CancellablePromise > = new Map(); -function findStoreBySchemaId(id: SchemaEntry['id']) { - return [...dbSchemaStoreMap.values()].find((entry) => - get(entry).data.has(id), - ); -} - function setDBSchemaStore( connectionId: Connection['id'], - schemas: SchemaResponse[], + schemas: Schema[], ): Writable { const schemaMap: DBSchemaStoreData['data'] = new Map(); schemas.forEach((schema) => { - schemaMap.set(schema.id, schema); + schemaMap.set(schema.oid, schema); }); const storeValue: DBSchemaStoreData = { requestStatus: { state: 'success' }, @@ -63,12 +55,12 @@ function setDBSchemaStore( function updateSchemaInDBSchemaStore( connectionId: Connection['id'], - schema: SchemaResponse, + schema: Schema, ) { const store = dbSchemaStoreMap.get(connectionId); if (store) { store.update((value) => { - value.data?.set(schema.id, schema); + value.data?.set(schema.oid, schema); return { ...value, data: new Map(value.data), @@ -79,7 +71,7 @@ function updateSchemaInDBSchemaStore( function removeSchemaInDBSchemaStore( connectionId: Connection['id'], - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ) { const store = dbSchemaStoreMap.get(connectionId); if (store) { @@ -95,36 +87,16 @@ function removeSchemaInDBSchemaStore( export function addCountToSchemaNumTables( connection: Connection, - schema: SchemaEntry, + schema: Schema, count: number, ) { const store = dbSchemaStoreMap.get(connection.id); if (store) { store.update((value) => { - const schemaToModify = value.data.get(schema.id); - if (schemaToModify) { - schemaToModify.num_tables += count; - value.data.set(schema.id, schemaToModify); - } - return { - ...value, - data: new Map(value.data), - }; - }); - } -} - -export function addCountToSchemaNumExplorations( - schemaId: SchemaEntry['id'], - count: number, -) { - const store = findStoreBySchemaId(schemaId); - if (store) { - store.update((value) => { - const schemaToModify = value.data.get(schemaId); + const schemaToModify = value.data.get(schema.oid); if (schemaToModify) { - schemaToModify.num_queries += count; - value.data.set(schemaId, schemaToModify); + schemaToModify.table_count += count; + value.data.set(schema.oid, schemaToModify); } return { ...value, @@ -153,10 +125,9 @@ async function refetchSchemasForDB( dbSchemasRequestMap.get(connectionId)?.cancel(); - const schemaRequest = schemasApi.list(connectionId); + const schemaRequest = api.schemas.list({ database_id: connectionId }).run(); dbSchemasRequestMap.set(connectionId, schemaRequest); - const response = await schemaRequest; - const schemas = response?.results || []; + const schemas = await schemaRequest; const dbSchemasStore = setDBSchemaStore(connectionId, schemas); @@ -201,8 +172,8 @@ function getSchemasStoreForDB( export function getSchemaInfo( connectionId: Connection['id'], - schemaId: SchemaEntry['id'], -): SchemaEntry | undefined { + schemaId: Schema['oid'], +): Schema | undefined { const store = dbSchemaStoreMap.get(connectionId); if (!store) { return undefined; @@ -212,32 +183,51 @@ export function getSchemaInfo( export async function createSchema( connectionId: Connection['id'], - schemaName: SchemaEntry['name'], - description: SchemaEntry['description'], -): Promise { - const response = await schemasApi.add({ - name: schemaName, + name: Schema['name'], + description: Schema['description'], +): Promise { + const schemaOid = await api.schemas + .add({ + database_id: connectionId, + name, + description, + }) + .run(); + updateSchemaInDBSchemaStore(connectionId, { + oid: schemaOid, + name, description, - connectionId, + table_count: 0, }); - updateSchemaInDBSchemaStore(connectionId, response); - return response; } export async function updateSchema( connectionId: Connection['id'], - schema: SchemaEntry, -): Promise { - const response = await schemasApi.update(schema); - updateSchemaInDBSchemaStore(connectionId, response); - return response; + schema: Schema, +): Promise { + await api.schemas + .patch({ + database_id: connectionId, + schema_oid: schema.oid, + patch: { + name: schema.name, + description: schema.description, + }, + }) + .run(); + updateSchemaInDBSchemaStore(connectionId, schema); } export async function deleteSchema( connectionId: Connection['id'], - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ): Promise { - await schemasApi.delete(schemaId); + await api.schemas + .delete({ + database_id: connectionId, + schema_oid: schemaId, + }) + .run(); removeSchemaInDBSchemaStore(connectionId, schemaId); } @@ -264,7 +254,7 @@ export const schemas: Readable = derived( }, ); -export const currentSchema: Readable = derived( +export const currentSchema: Readable = derived( [currentSchemaId, schemas], ([$currentSchemaId, $schemas]) => $currentSchemaId ? $schemas.data.get($currentSchemaId) : undefined, diff --git a/mathesar_ui/src/stores/storeBasedUrls.ts b/mathesar_ui/src/stores/storeBasedUrls.ts index 2ac9ba02a5..7c858b6bd2 100644 --- a/mathesar_ui/src/stores/storeBasedUrls.ts +++ b/mathesar_ui/src/stores/storeBasedUrls.ts @@ -31,7 +31,7 @@ export const storeToGetRecordPageUrl = derived( recordId: unknown; }): string | undefined { const d = connectionId ?? connection?.id; - const s = schemaId ?? schema?.id; + const s = schemaId ?? schema?.oid; const t = tableId ?? table?.id; const r = recordId ?? undefined; if ( @@ -61,7 +61,7 @@ export const storeToGetTablePageUrl = derived( tableId?: number; }): string | undefined { const d = connectionId ?? connection?.id; - const s = schemaId ?? schema?.id; + const s = schemaId ?? schema?.oid; const t = tableId ?? table?.id; if (d === undefined || s === undefined || t === undefined) { return undefined; diff --git a/mathesar_ui/src/stores/tables.ts b/mathesar_ui/src/stores/tables.ts index 047d37d183..219cc4407c 100644 --- a/mathesar_ui/src/stores/tables.ts +++ b/mathesar_ui/src/stores/tables.ts @@ -35,7 +35,8 @@ import { patchAPI, postAPI, } from '@mathesar/api/rest/utils/requestUtils'; -import type { DBObjectEntry, Database, SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; +import type { DBObjectEntry, Database } from '@mathesar/AppTypes'; import { invalidIf } from '@mathesar/components/form'; import type { AtLeastOne } from '@mathesar/typeUtils'; import { preloadCommonData } from '@mathesar/utils/preloadData'; @@ -55,11 +56,11 @@ export interface DBTablesStoreData { } const schemaTablesStoreMap: Map< - SchemaEntry['id'], + Schema['oid'], Writable > = new Map(); const schemaTablesRequestMap: Map< - SchemaEntry['id'], + Schema['oid'], CancellablePromise> > = new Map(); @@ -68,7 +69,7 @@ function sortedTableEntries(tableEntries: TableEntry[]): TableEntry[] { } function setSchemaTablesStore( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], tableEntries?: TableEntry[], ): Writable { const tables: DBTablesStoreData['data'] = new Map(); @@ -93,14 +94,12 @@ function setSchemaTablesStore( return store; } -export function removeTablesInSchemaTablesStore( - schemaId: SchemaEntry['id'], -): void { +export function removeTablesInSchemaTablesStore(schemaId: Schema['oid']): void { schemaTablesStoreMap.delete(schemaId); } export async function refetchTablesForSchema( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ): Promise { const store = schemaTablesStoreMap.get(schemaId); if (!store) { @@ -143,7 +142,7 @@ export async function refetchTablesForSchema( let preload = true; export function getTablesStoreForSchema( - schemaId: SchemaEntry['id'], + schemaId: Schema['oid'], ): Writable { let store = schemaTablesStoreMap.get(schemaId); if (!store) { @@ -195,7 +194,7 @@ function findAndUpdateTableStore(id: TableEntry['id'], tableEntry: TableEntry) { export function deleteTable( database: Database, - schema: SchemaEntry, + schema: Schema, tableId: TableEntry['id'], ): CancellablePromise { const promise = deleteAPI(`/api/db/v0/tables/${tableId}/`); @@ -203,7 +202,7 @@ export function deleteTable( (resolve, reject) => { void promise.then((value) => { addCountToSchemaNumTables(database, schema, -1); - schemaTablesStoreMap.get(schema.id)?.update((tableStoreData) => { + schemaTablesStoreMap.get(schema.oid)?.update((tableStoreData) => { tableStoreData.data.delete(tableId); return { ...tableStoreData, @@ -242,14 +241,14 @@ export function updateTableMetaData( export function createTable( database: Database, - schema: SchemaEntry, + schema: Schema, tableArgs: { name?: string; dataFiles?: [number, ...number[]]; }, ): CancellablePromise { const promise = postAPI('/api/db/v0/tables/', { - schema: schema.id, + schema: schema.oid, name: tableArgs.name, data_files: tableArgs.dataFiles, }); @@ -257,7 +256,7 @@ export function createTable( (resolve, reject) => { void promise.then((value) => { addCountToSchemaNumTables(database, schema, 1); - schemaTablesStoreMap.get(schema.id)?.update((existing) => { + schemaTablesStoreMap.get(schema.oid)?.update((existing) => { const tableEntryMap: DBTablesStoreData['data'] = new Map(); sortedTableEntries([...existing.data.values(), value]).forEach( (entry) => { diff --git a/mathesar_ui/src/stores/users.ts b/mathesar_ui/src/stores/users.ts index 8609761322..90e4c07a82 100644 --- a/mathesar_ui/src/stores/users.ts +++ b/mathesar_ui/src/stores/users.ts @@ -11,7 +11,8 @@ import userApi, { type UserRole, } from '@mathesar/api/rest/users'; import type { RequestStatus } from '@mathesar/api/rest/utils/requestUtils'; -import type { Database, SchemaEntry } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; +import type { Database } from '@mathesar/AppTypes'; import { getErrorMessage } from '@mathesar/utils/errors'; import { type AccessOperation, @@ -54,7 +55,7 @@ export class UserModel { hasPermission( dbObject: { database?: Pick; - schema?: Pick; + schema?: Pick; }, operation: AccessOperation, ): boolean { @@ -69,7 +70,7 @@ export class UserModel { } const roles: UserRole[] = []; if (schema) { - const userSchemaRole = this.schemaRoles.get(schema.id); + const userSchemaRole = this.schemaRoles.get(schema.oid); if (userSchemaRole) { roles.push(userSchemaRole.role); } @@ -87,8 +88,8 @@ export class UserModel { return this.databaseRoles.get(database.id); } - getRoleForSchema(schema: Pick) { - return this.schemaRoles.get(schema.id); + getRoleForSchema(schema: Pick) { + return this.schemaRoles.get(schema.oid); } hasDirectDbAccess(database: Pick) { @@ -99,14 +100,11 @@ export class UserModel { return this.hasDirectDbAccess(database) || this.isSuperUser; } - hasDirectSchemaAccess(schema: Pick) { - return this.schemaRoles.has(schema.id); + hasDirectSchemaAccess(schema: Pick) { + return this.schemaRoles.has(schema.oid); } - hasSchemaAccess( - database: Pick, - schema: Pick, - ) { + hasSchemaAccess(database: Pick, schema: Pick) { return this.hasDbAccess(database) || this.hasDirectSchemaAccess(schema); } @@ -298,10 +296,10 @@ class WritableUsersStore { async addSchemaRoleForUser( userId: number, - schema: Pick, + schema: Pick, role: UserRole, ) { - const schemaRole = await userApi.addSchemaRole(userId, schema.id, role); + const schemaRole = await userApi.addSchemaRole(userId, schema.oid, role); this.users.update((users) => users.map((user) => { if (user.id === userId) { @@ -313,10 +311,7 @@ class WritableUsersStore { void this.fetchUsersSilently(); } - async removeSchemaAccessForUser( - userId: number, - schema: Pick, - ) { + async removeSchemaAccessForUser(userId: number, schema: Pick) { const user = get(this.users).find((entry) => entry.id === userId); const schemaRole = user?.getRoleForSchema(schema); if (schemaRole) { @@ -345,7 +340,7 @@ class WritableUsersStore { ); } - getNormalUsersWithDirectSchemaRole(schema: Pick) { + getNormalUsersWithDirectSchemaRole(schema: Pick) { return derived(this.users, ($users) => $users.filter( (user) => !user.isSuperUser && user.hasDirectSchemaAccess(schema), @@ -353,7 +348,7 @@ class WritableUsersStore { ); } - getNormalUsersWithoutDirectSchemaRole(schema: Pick) { + getNormalUsersWithoutDirectSchemaRole(schema: Pick) { return derived(this.users, ($users) => $users.filter( (user) => !user.isSuperUser && !user.hasDirectSchemaAccess(schema), @@ -363,7 +358,7 @@ class WritableUsersStore { getUsersWithAccessToSchema( database: Pick, - schema: Pick, + schema: Pick, ) { return derived(this.users, ($users) => $users.filter((user) => user.hasSchemaAccess(database, schema)), diff --git a/mathesar_ui/src/systems/data-explorer/result-pane/QueryRunErrors.svelte b/mathesar_ui/src/systems/data-explorer/result-pane/QueryRunErrors.svelte index cca067558f..6efc59e488 100644 --- a/mathesar_ui/src/systems/data-explorer/result-pane/QueryRunErrors.svelte +++ b/mathesar_ui/src/systems/data-explorer/result-pane/QueryRunErrors.svelte @@ -89,7 +89,7 @@ class="btn btn-secondary" href={getExplorationEditorPageUrl( $currentDatabase.id, - $currentSchema.id, + $currentSchema.oid, $query.id, )} > diff --git a/mathesar_ui/src/systems/table-view/table-inspector/table/TableActions.svelte b/mathesar_ui/src/systems/table-view/table-inspector/table/TableActions.svelte index f24e68428b..4c36f76d7a 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/table/TableActions.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/table/TableActions.svelte @@ -34,7 +34,7 @@ $currentDatabase && $currentSchema ? createDataExplorerUrlToExploreATable( $currentDatabase?.id, - $currentSchema.id, + $currentSchema.oid, { id: $tabularData.id, name: $tables.data.get($tabularData.id)?.name ?? '', @@ -47,7 +47,7 @@ } return constructDataExplorerUrlToSummarizeFromGroup( $currentDatabase.id, - $currentSchema.id, + $currentSchema.oid, { baseTable: { id, name: $currentTable.name }, columns: $columns, @@ -72,7 +72,7 @@ const schema = $currentSchema; if (database && schema) { await deleteTable(database, schema, $tabularData.id); - router.goto(getSchemaPageUrl(database.id, schema.id), true); + router.goto(getSchemaPageUrl(database.id, schema.oid), true); } }, }); diff --git a/mathesar_ui/src/utils/preloadData.ts b/mathesar_ui/src/utils/preloadData.ts index 09a3acc76e..c857d24128 100644 --- a/mathesar_ui/src/utils/preloadData.ts +++ b/mathesar_ui/src/utils/preloadData.ts @@ -2,11 +2,12 @@ import type { Connection } from '@mathesar/api/rest/connections'; import type { QueryInstance } from '@mathesar/api/rest/types/queries'; import type { TableEntry } from '@mathesar/api/rest/types/tables'; import type { User } from '@mathesar/api/rest/users'; -import type { AbstractTypeResponse, SchemaResponse } from '@mathesar/AppTypes'; +import type { Schema } from '@mathesar/api/rpc/schemas'; +import type { AbstractTypeResponse } from '@mathesar/AppTypes'; export interface CommonData { connections: Connection[]; - schemas: SchemaResponse[]; + schemas: Schema[]; tables: TableEntry[]; queries: QueryInstance[]; current_connection: Connection['id'] | null;