mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-04-28 04:20:04 -05:00
Breaking: nest records in output of RecordAPI.list to contain cursor now and potentially more in the future.
Also update all the client libraries to accept the new format.
This commit is contained in:
@@ -108,6 +108,17 @@ class Pagination {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ListResponse {
|
||||||
|
final String? cursor;
|
||||||
|
final List<Map<String, dynamic>> records;
|
||||||
|
|
||||||
|
const ListResponse({this.cursor, required this.records});
|
||||||
|
|
||||||
|
ListResponse.fromJson(Map<String, dynamic> json)
|
||||||
|
: cursor = json['cursor'],
|
||||||
|
records = (json['records'] as List).cast<Map<String, dynamic>>();
|
||||||
|
}
|
||||||
|
|
||||||
abstract class RecordId {
|
abstract class RecordId {
|
||||||
@override
|
@override
|
||||||
String toString();
|
String toString();
|
||||||
@@ -278,7 +289,7 @@ class RecordApi {
|
|||||||
|
|
||||||
const RecordApi(this._client, this._name);
|
const RecordApi(this._client, this._name);
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> list({
|
Future<ListResponse> list({
|
||||||
Pagination? pagination,
|
Pagination? pagination,
|
||||||
List<String>? order,
|
List<String>? order,
|
||||||
List<String>? filters,
|
List<String>? filters,
|
||||||
@@ -310,7 +321,7 @@ class RecordApi {
|
|||||||
queryParams: params,
|
queryParams: params,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (response.data as List).cast<Map<String, dynamic>>();
|
return ListResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> read(RecordId id) async {
|
Future<Map<String, dynamic>> read(RecordId id) async {
|
||||||
|
|||||||
@@ -159,25 +159,27 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
final records = await api.list(
|
final response = await api.list(
|
||||||
filters: ['text_not_null=${messages[0]}'],
|
filters: ['text_not_null=${messages[0]}'],
|
||||||
);
|
);
|
||||||
expect(records.length, 1);
|
expect(response.records.length, 1);
|
||||||
expect(records[0]['text_not_null'], messages[0]);
|
expect(response.records[0]['text_not_null'], messages[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
final recordsAsc = await api.list(
|
final recordsAsc = (await api.list(
|
||||||
order: ['+text_not_null'],
|
order: ['+text_not_null'],
|
||||||
filters: ['text_not_null[like]=% =?&${now}'],
|
filters: ['text_not_null[like]=% =?&${now}'],
|
||||||
);
|
))
|
||||||
|
.records;
|
||||||
expect(recordsAsc.map((el) => el['text_not_null']),
|
expect(recordsAsc.map((el) => el['text_not_null']),
|
||||||
orderedEquals(messages));
|
orderedEquals(messages));
|
||||||
|
|
||||||
final recordsDesc = await api.list(
|
final recordsDesc = (await api.list(
|
||||||
order: ['-text_not_null'],
|
order: ['-text_not_null'],
|
||||||
filters: ['text_not_null[like]=%${now}'],
|
filters: ['text_not_null[like]=%${now}'],
|
||||||
);
|
))
|
||||||
|
.records;
|
||||||
expect(recordsDesc.map((el) => el['text_not_null']).toList().reversed,
|
expect(recordsDesc.map((el) => el['text_not_null']).toList().reversed,
|
||||||
orderedEquals(messages));
|
orderedEquals(messages));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,26 @@ public class Pagination {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representation of ListResponse JSON objects.
|
||||||
|
/// </summary>
|
||||||
|
// @JsonSerializable(explicitToJson: true)
|
||||||
|
public class ListResponse<T> {
|
||||||
|
/// <summary>List cursor for subsequent fetches.</summary>
|
||||||
|
public string? cursor { get; }
|
||||||
|
/// <summary>The actual records.</summary>
|
||||||
|
public List<T> records { get; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public ListResponse(
|
||||||
|
string? cursor,
|
||||||
|
List<T>? records
|
||||||
|
) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
this.records = records ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Realtime event for change subscriptions.</summary>
|
/// <summary>Realtime event for change subscriptions.</summary>
|
||||||
public abstract class Event {
|
public abstract class Event {
|
||||||
/// <summary>Get associated record value as JSON object.</summary>
|
/// <summary>Get associated record value as JSON object.</summary>
|
||||||
@@ -264,13 +284,13 @@ public class RecordApi {
|
|||||||
/// <param name="filters">Results filters, e.g. "col0[gte]=100".</param>
|
/// <param name="filters">Results filters, e.g. "col0[gte]=100".</param>
|
||||||
[RequiresDynamicCode(DynamicCodeMessage)]
|
[RequiresDynamicCode(DynamicCodeMessage)]
|
||||||
[RequiresUnreferencedCode(UnreferencedCodeMessage)]
|
[RequiresUnreferencedCode(UnreferencedCodeMessage)]
|
||||||
public async Task<List<T>> List<T>(
|
public async Task<ListResponse<T>> List<T>(
|
||||||
Pagination? pagination,
|
Pagination? pagination,
|
||||||
List<string>? order,
|
List<string>? order,
|
||||||
List<string>? filters
|
List<string>? filters
|
||||||
) {
|
) {
|
||||||
string json = await (await ListImpl(pagination, order, filters)).ReadAsStringAsync();
|
string json = await (await ListImpl(pagination, order, filters)).ReadAsStringAsync();
|
||||||
return JsonSerializer.Deserialize<List<T>>(json) ?? [];
|
return JsonSerializer.Deserialize<ListResponse<T>>(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -280,14 +300,14 @@ public class RecordApi {
|
|||||||
/// <param name="order">Sort results by the given columns in ascending/descending order, e.g. "-col_name".</param>
|
/// <param name="order">Sort results by the given columns in ascending/descending order, e.g. "-col_name".</param>
|
||||||
/// <param name="filters">Results filters, e.g. "col0[gte]=100".</param>
|
/// <param name="filters">Results filters, e.g. "col0[gte]=100".</param>
|
||||||
/// <param name="jsonTypeInfo">Serialization type info for AOT mode.</param>
|
/// <param name="jsonTypeInfo">Serialization type info for AOT mode.</param>
|
||||||
public async Task<List<T>> List<T>(
|
public async Task<ListResponse<T>> List<T>(
|
||||||
Pagination? pagination,
|
Pagination? pagination,
|
||||||
List<string>? order,
|
List<string>? order,
|
||||||
List<string>? filters,
|
List<string>? filters,
|
||||||
JsonTypeInfo<List<T>> jsonTypeInfo
|
JsonTypeInfo<ListResponse<T>> jsonTypeInfo
|
||||||
) {
|
) {
|
||||||
string json = await (await ListImpl(pagination, order, filters)).ReadAsStringAsync();
|
string json = await (await ListImpl(pagination, order, filters)).ReadAsStringAsync();
|
||||||
return JsonSerializer.Deserialize<List<T>>(json, jsonTypeInfo) ?? [];
|
return JsonSerializer.Deserialize<ListResponse<T>>(json, jsonTypeInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpContent> ListImpl(
|
private async Task<HttpContent> ListImpl(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class SimpleStrict {
|
|||||||
|
|
||||||
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
[JsonSerializable(typeof(SimpleStrict))]
|
[JsonSerializable(typeof(SimpleStrict))]
|
||||||
[JsonSerializable(typeof(List<SimpleStrict>))]
|
[JsonSerializable(typeof(ListResponse<SimpleStrict>))]
|
||||||
internal partial class SerializeSimpleStrictContext : JsonSerializerContext {
|
internal partial class SerializeSimpleStrictContext : JsonSerializerContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,29 +144,31 @@ public class ClientTest : IClassFixture<ClientTestFixture> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
List<SimpleStrict> records = await api.List<SimpleStrict>(
|
ListResponse<SimpleStrict> response = await api.List<SimpleStrict>(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
[$"text_not_null={messages[0]}"]
|
[$"text_not_null={messages[0]}"]
|
||||||
)!;
|
)!;
|
||||||
Assert.Single(records);
|
Assert.Single(response.records);
|
||||||
Assert.Equal(messages[0], records[0].text_not_null);
|
Assert.Equal(messages[0], response.records[0].text_not_null);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var recordsAsc = await api.List<SimpleStrict>(
|
var responseAsc = await api.List<SimpleStrict>(
|
||||||
null,
|
null,
|
||||||
["+text_not_null"],
|
["+text_not_null"],
|
||||||
[$"text_not_null[like]=% =?&{suffix}"]
|
[$"text_not_null[like]=% =?&{suffix}"]
|
||||||
)!;
|
)!;
|
||||||
|
var recordsAsc = responseAsc.records;
|
||||||
Assert.Equal(messages.Count, recordsAsc.Count);
|
Assert.Equal(messages.Count, recordsAsc.Count);
|
||||||
Assert.Equal(messages, recordsAsc.ConvertAll((e) => e.text_not_null));
|
Assert.Equal(messages, recordsAsc.ConvertAll((e) => e.text_not_null));
|
||||||
|
|
||||||
var recordsDesc = await api.List<SimpleStrict>(
|
var responseDesc = await api.List<SimpleStrict>(
|
||||||
null,
|
null,
|
||||||
["-text_not_null"],
|
["-text_not_null"],
|
||||||
[$"text_not_null[like]=%{suffix}"]
|
[$"text_not_null[like]=%{suffix}"]
|
||||||
)!;
|
)!;
|
||||||
|
var recordsDesc = responseDesc.records;
|
||||||
Assert.Equal(messages.Count, recordsDesc.Count);
|
Assert.Equal(messages.Count, recordsDesc.Count);
|
||||||
recordsDesc.Reverse();
|
recordsDesc.Reverse();
|
||||||
Assert.Equal(messages, recordsDesc.ConvertAll((e) => e.text_not_null));
|
Assert.Equal(messages, recordsDesc.ConvertAll((e) => e.text_not_null));
|
||||||
@@ -197,13 +199,13 @@ public class ClientTest : IClassFixture<ClientTestFixture> {
|
|||||||
var id = ids[0];
|
var id = ids[0];
|
||||||
await api.Delete(id);
|
await api.Delete(id);
|
||||||
|
|
||||||
var records = await api.List<SimpleStrict>(
|
var response = await api.List<SimpleStrict>(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
[$"text_not_null[like]=%{suffix}"]
|
[$"text_not_null[like]=%{suffix}"]
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
Assert.Single(records);
|
Assert.Single(response.records);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,32 +235,34 @@ public class ClientTest : IClassFixture<ClientTestFixture> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
List<SimpleStrict> records = await api.List(
|
ListResponse<SimpleStrict> response = await api.List(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
[$"text_not_null={messages[0]}"],
|
[$"text_not_null={messages[0]}"],
|
||||||
SerializeSimpleStrictContext.Default.ListSimpleStrict
|
SerializeSimpleStrictContext.Default.ListResponseSimpleStrict
|
||||||
)!;
|
)!;
|
||||||
Assert.Single(records);
|
Assert.Single(response.records);
|
||||||
Assert.Equal(messages[0], records[0].text_not_null);
|
Assert.Equal(messages[0], response.records[0].text_not_null);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var recordsAsc = await api.List(
|
var responseAsc = await api.List(
|
||||||
null,
|
null,
|
||||||
["+text_not_null"],
|
["+text_not_null"],
|
||||||
[$"text_not_null[like]=% =?&{suffix}"],
|
[$"text_not_null[like]=% =?&{suffix}"],
|
||||||
SerializeSimpleStrictContext.Default.ListSimpleStrict
|
SerializeSimpleStrictContext.Default.ListResponseSimpleStrict
|
||||||
)!;
|
)!;
|
||||||
|
var recordsAsc = responseAsc.records;
|
||||||
Assert.Equal(messages.Count, recordsAsc.Count);
|
Assert.Equal(messages.Count, recordsAsc.Count);
|
||||||
Assert.Equal(messages, recordsAsc.ConvertAll((e) => e.text_not_null));
|
Assert.Equal(messages, recordsAsc.ConvertAll((e) => e.text_not_null));
|
||||||
|
|
||||||
var recordsDesc = await api.List(
|
var responseDesc = await api.List(
|
||||||
null,
|
null,
|
||||||
["-text_not_null"],
|
["-text_not_null"],
|
||||||
[$"text_not_null[like]=%{suffix}"],
|
[$"text_not_null[like]=%{suffix}"],
|
||||||
SerializeSimpleStrictContext.Default.ListSimpleStrict
|
SerializeSimpleStrictContext.Default.ListResponseSimpleStrict
|
||||||
)!;
|
)!;
|
||||||
|
var recordsDesc = responseDesc.records;
|
||||||
Assert.Equal(messages.Count, recordsDesc.Count);
|
Assert.Equal(messages.Count, recordsDesc.Count);
|
||||||
recordsDesc.Reverse();
|
recordsDesc.Reverse();
|
||||||
Assert.Equal(messages, recordsDesc.ConvertAll((e) => e.text_not_null));
|
Assert.Equal(messages, recordsDesc.ConvertAll((e) => e.text_not_null));
|
||||||
@@ -293,14 +297,14 @@ public class ClientTest : IClassFixture<ClientTestFixture> {
|
|||||||
var id = ids[0];
|
var id = ids[0];
|
||||||
await api.Delete(id);
|
await api.Delete(id);
|
||||||
|
|
||||||
var records = await api.List(
|
var response = await api.List(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
[$"text_not_null[like]=%{suffix}"],
|
[$"text_not_null[like]=%{suffix}"],
|
||||||
SerializeSimpleStrictContext.Default.ListSimpleStrict
|
SerializeSimpleStrictContext.Default.ListResponseSimpleStrict
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
Assert.Single(records);
|
Assert.Single(response.records);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ def test_records(trailbase: TrailBaseFixture):
|
|||||||
ids.append(api.create({"text_not_null": msg}))
|
ids.append(api.create({"text_not_null": msg}))
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
records = api.list(
|
response = api.list(
|
||||||
filters=[f"text_not_null={messages[0]}"],
|
filters=[f"text_not_null={messages[0]}"],
|
||||||
)
|
)
|
||||||
|
records = response.records
|
||||||
assert len(records) == 1
|
assert len(records) == 1
|
||||||
assert records[0]["text_not_null"] == messages[0]
|
assert records[0]["text_not_null"] == messages[0]
|
||||||
|
|
||||||
@@ -126,14 +127,14 @@ def test_records(trailbase: TrailBaseFixture):
|
|||||||
filters=[f"text_not_null[like]=% =?&{now}"],
|
filters=[f"text_not_null[like]=% =?&{now}"],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert [el["text_not_null"] for el in recordsAsc] == messages
|
assert [el["text_not_null"] for el in recordsAsc.records] == messages
|
||||||
|
|
||||||
recordsDesc = api.list(
|
recordsDesc = api.list(
|
||||||
order=["-text_not_null"],
|
order=["-text_not_null"],
|
||||||
filters=[f"text_not_null[like]=%{now}"],
|
filters=[f"text_not_null[like]=%{now}"],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert [el["text_not_null"] for el in recordsDesc] == list(reversed(messages))
|
assert [el["text_not_null"] for el in recordsDesc.records] == list(reversed(messages))
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
record = api.read(ids[0])
|
record = api.read(ids[0])
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import json
|
|||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TypeAlias, Any
|
from typing import TypeAlias, Any, cast
|
||||||
|
|
||||||
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
|
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
|
||||||
|
|
||||||
@@ -55,6 +55,24 @@ class User:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ListResponse:
|
||||||
|
cursor: str | None
|
||||||
|
records: list[dict[str, object]]
|
||||||
|
|
||||||
|
def __init__(self, cursor: str | None, records: list[dict[str, object]]) -> None:
|
||||||
|
self.cursor = cursor
|
||||||
|
self.records = records
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromJson(json: dict[str, "JSON"]) -> "ListResponse":
|
||||||
|
cursor = json["cursor"]
|
||||||
|
assert isinstance(cursor, str | None)
|
||||||
|
records = json["records"]
|
||||||
|
assert isinstance(records, list)
|
||||||
|
|
||||||
|
return ListResponse(cursor, cast(list[dict[str, object]], records))
|
||||||
|
|
||||||
|
|
||||||
class Tokens:
|
class Tokens:
|
||||||
auth: str
|
auth: str
|
||||||
refresh: str | None
|
refresh: str | None
|
||||||
@@ -390,7 +408,7 @@ class RecordApi:
|
|||||||
filters: list[str] | None = None,
|
filters: list[str] | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
) -> list[dict[str, object]]:
|
) -> ListResponse:
|
||||||
params: dict[str, str] = {}
|
params: dict[str, str] = {}
|
||||||
|
|
||||||
if cursor != None:
|
if cursor != None:
|
||||||
@@ -411,7 +429,7 @@ class RecordApi:
|
|||||||
params[nameOp] = value
|
params[nameOp] = value
|
||||||
|
|
||||||
response = self._client.fetch(f"{self._recordApi}/{self._name}", queryParams=params)
|
response = self._client.fetch(f"{self._recordApi}/{self._name}", queryParams=params)
|
||||||
return response.json()
|
return ListResponse.fromJson(response.json())
|
||||||
|
|
||||||
def read(self, recordId: RecordId | str | int) -> dict[str, object]:
|
def read(self, recordId: RecordId | str | int) -> dict[str, object]:
|
||||||
id = repr(recordId) if isinstance(recordId, RecordId) else f"{recordId}"
|
id = repr(recordId) if isinstance(recordId, RecordId) else f"{recordId}"
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! A client library to connect to a TrailBase server via HTTP.
|
||||||
|
//!
|
||||||
|
//! TrailBase is a sub-millisecond, open-source application server with type-safe APIs, built-in
|
||||||
|
//! JS/ES6/TS runtime, realtime, auth, and admin UI built on Rust, SQLite & V8.
|
||||||
|
|
||||||
#![allow(clippy::needless_return)]
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
use eventsource_stream::Eventsource;
|
use eventsource_stream::Eventsource;
|
||||||
@@ -26,12 +31,16 @@ pub enum Error {
|
|||||||
Precondition(&'static str),
|
Precondition(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the currently logged-in user.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub sub: String,
|
pub sub: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds the tokens minted by the server on login.
|
||||||
|
///
|
||||||
|
/// It is also the exact JSON serialization format.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Tokens {
|
pub struct Tokens {
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
@@ -53,6 +62,12 @@ pub enum DbEvent {
|
|||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ListResponse<T> {
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
pub records: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait RecordId<'a> {
|
pub trait RecordId<'a> {
|
||||||
fn serialized_id(self) -> Cow<'a, str>;
|
fn serialized_id(self) -> Cow<'a, str>;
|
||||||
}
|
}
|
||||||
@@ -151,7 +166,7 @@ impl RecordApi {
|
|||||||
pagination: Option<Pagination>,
|
pagination: Option<Pagination>,
|
||||||
order: Option<&[&str]>,
|
order: Option<&[&str]>,
|
||||||
filters: Option<&[&str]>,
|
filters: Option<&[&str]>,
|
||||||
) -> Result<Vec<T>, Error> {
|
) -> Result<ListResponse<T>, Error> {
|
||||||
let mut params: Vec<(Cow<'static, str>, Cow<'static, str>)> = vec![];
|
let mut params: Vec<(Cow<'static, str>, Cow<'static, str>)> = vec![];
|
||||||
if let Some(pagination) = pagination {
|
if let Some(pagination) = pagination {
|
||||||
if let Some(cursor) = pagination.cursor {
|
if let Some(cursor) = pagination.cursor {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use trailbase_client::{Client, DbEvent};
|
use trailbase_client::{Client, DbEvent, Pagination};
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
child: std::process::Child,
|
child: std::process::Child,
|
||||||
@@ -122,12 +122,26 @@ async fn records_test() {
|
|||||||
// List one specific message.
|
// List one specific message.
|
||||||
let filter = format!("text_not_null={}", messages[0]);
|
let filter = format!("text_not_null={}", messages[0]);
|
||||||
let filters = vec![filter.as_str()];
|
let filters = vec![filter.as_str()];
|
||||||
let records = api
|
let response = api
|
||||||
.list::<serde_json::Value>(None, None, Some(filters.as_slice()))
|
.list::<serde_json::Value>(None, None, Some(filters.as_slice()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(records.len(), 1);
|
assert_eq!(response.records.len(), 1);
|
||||||
|
|
||||||
|
let second_response = api
|
||||||
|
.list::<serde_json::Value>(
|
||||||
|
Some(Pagination {
|
||||||
|
cursor: response.cursor,
|
||||||
|
limit: None,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
Some(filters.as_slice()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(second_response.records.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -136,7 +150,8 @@ async fn records_test() {
|
|||||||
let records_ascending: Vec<SimpleStrict> = api
|
let records_ascending: Vec<SimpleStrict> = api
|
||||||
.list(None, Some(&["+text_not_null"]), Some(&[&filter]))
|
.list(None, Some(&["+text_not_null"]), Some(&[&filter]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.records;
|
||||||
|
|
||||||
let messages_ascending: Vec<_> = records_ascending
|
let messages_ascending: Vec<_> = records_ascending
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -147,7 +162,8 @@ async fn records_test() {
|
|||||||
let records_descending: Vec<SimpleStrict> = api
|
let records_descending: Vec<SimpleStrict> = api
|
||||||
.list(None, Some(&["-text_not_null"]), Some(&[&filter]))
|
.list(None, Some(&["-text_not_null"]), Some(&[&filter]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.records;
|
||||||
|
|
||||||
let messages_descending: Vec<_> = records_descending
|
let messages_descending: Vec<_> = records_descending
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -94,8 +94,9 @@ class _LandingState extends State<Landing> {
|
|||||||
|
|
||||||
Future<void> _fetchArticles() async {
|
Future<void> _fetchArticles() async {
|
||||||
try {
|
try {
|
||||||
final records = await _articlesApi.list();
|
final response = await _articlesApi.list();
|
||||||
_articlesCtrl.add(records.map((r) => Article.fromJson(r)).toList());
|
_articlesCtrl
|
||||||
|
.add(response.records.map((r) => Article.fromJson(r)).toList());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_articlesCtrl.addError(err);
|
_articlesCtrl.addError(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"solid-icons": "^1.1.0",
|
"solid-icons": "^1.1.0",
|
||||||
"solid-js": "^1.9.4",
|
"solid-js": "^1.9.4",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"trailbase": "^0.1.4"
|
"trailbase": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/solid-js": "^5.0.2",
|
"@astrojs/solid-js": "^5.0.2",
|
||||||
|
|||||||
@@ -133,9 +133,10 @@ function AssignNewUsername(props: { client: Client }) {
|
|||||||
|
|
||||||
export function ArticleList() {
|
export function ArticleList() {
|
||||||
const client = useStore($client);
|
const client = useStore($client);
|
||||||
const [articles] = createResource(client, (client) =>
|
const [articles] = createResource(client, async (client) => {
|
||||||
client?.records("articles_view").list<Article>(),
|
const response = await client?.records("articles_view").list<Article>();
|
||||||
);
|
return response.records;
|
||||||
|
});
|
||||||
const profile = useStore($profile);
|
const profile = useStore($profile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +206,7 @@ export function ArticlePage() {
|
|||||||
<Match when={article.error}>Failed to load: {`${article.error}`}</Match>
|
<Match when={article.error}>Failed to load: {`${article.error}`}</Match>
|
||||||
|
|
||||||
<Match when={article()}>
|
<Match when={article()}>
|
||||||
<ArticlePageImpl article={article()!} />
|
<ArticlePageImpl article={article()!.records} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"format": "prettier -w src",
|
"format": "prettier -w src",
|
||||||
"read": "tsc && node dist/src/index.js",
|
"read": "node --loader ts-node/esm src/index.js",
|
||||||
"fill": "tsc && node dist/src/fill.js",
|
"fill": "node --loader ts-node/esm src/fill.js",
|
||||||
"check": "tsc --noEmit --skipLibCheck && eslint"
|
"check": "tsc --noEmit --skipLibCheck && eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"quicktype": "^23.0.170",
|
"quicktype": "^23.0.170",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,18 +8,28 @@ const client = new Client("http://localhost:4000");
|
|||||||
await client.login("admin@localhost", "secret");
|
await client.login("admin@localhost", "secret");
|
||||||
const api = client.records("movies");
|
const api = client.records("movies");
|
||||||
|
|
||||||
let movies = [];
|
// Start fresh: delete all existing movies.
|
||||||
do {
|
let cnt = 0;
|
||||||
movies = await api.list<Movie>({
|
while (true) {
|
||||||
|
const movies = await api.list<Movie>({
|
||||||
pagination: {
|
pagination: {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const movie of movies) {
|
const records = movies.records;
|
||||||
|
const length = records.length;
|
||||||
|
if (length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cnt += length;
|
||||||
|
|
||||||
|
for (const movie of records) {
|
||||||
await api.delete(movie.rank!);
|
await api.delete(movie.rank!);
|
||||||
}
|
}
|
||||||
} while (movies.length > 0);
|
}
|
||||||
|
|
||||||
|
console.log(`Cleaned up ${cnt} movies`);
|
||||||
|
|
||||||
const file = await readFile("../data/Top_1000_IMDb_movies_New_version.csv");
|
const file = await readFile("../data/Top_1000_IMDb_movies_New_version.csv");
|
||||||
const records = parse(file, {
|
const records = parse(file, {
|
||||||
@@ -41,3 +51,5 @@ for (const movie of records) {
|
|||||||
description: movie.description,
|
description: movie.description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Inserted ${records.length} movies`);
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ const m = await movies.list({
|
|||||||
filters: ["watch_time[lt]=120"],
|
filters: ["watch_time[lt]=120"],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(m);
|
console.log(m.records);
|
||||||
|
|||||||
@@ -3,3 +3,8 @@ backups/
|
|||||||
data/
|
data/
|
||||||
secrets/
|
secrets/
|
||||||
uploads/
|
uploads/
|
||||||
|
|
||||||
|
!migrations/
|
||||||
|
|
||||||
|
trailbase.js
|
||||||
|
trailbase.d.ts
|
||||||
|
|||||||
Generated
+10
-249
@@ -74,7 +74,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.1.4(astro@5.1.6(@types/node@16.18.123)(jiti@2.4.2)(rollup@4.30.1)(typescript@4.9.4)(yaml@2.7.0))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)))(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
version: 5.1.4(astro@5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))
|
||||||
'@nanostores/persistent':
|
'@nanostores/persistent':
|
||||||
specifier: ^0.10.2
|
specifier: ^0.10.2
|
||||||
version: 0.10.2(nanostores@0.11.3)
|
version: 0.10.2(nanostores@0.11.3)
|
||||||
@@ -83,7 +83,7 @@ importers:
|
|||||||
version: 0.5.0(nanostores@0.11.3)(solid-js@1.9.4)
|
version: 0.5.0(nanostores@0.11.3)(solid-js@1.9.4)
|
||||||
astro:
|
astro:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.1.6(@types/node@16.18.123)(jiti@2.4.2)(rollup@4.30.1)(typescript@4.9.4)(yaml@2.7.0)
|
version: 5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0)
|
||||||
astro-icon:
|
astro-icon:
|
||||||
specifier: ^1.1.5
|
specifier: ^1.1.5
|
||||||
version: 1.1.5
|
version: 1.1.5
|
||||||
@@ -98,20 +98,20 @@ importers:
|
|||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.17
|
specifier: ^3.4.17
|
||||||
version: 3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
version: 3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))
|
||||||
trailbase:
|
trailbase:
|
||||||
specifier: ^0.1.4
|
specifier: workspace:*
|
||||||
version: 0.1.4
|
version: link:../../../trailbase-core/js/client
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@astrojs/solid-js':
|
'@astrojs/solid-js':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2(@types/node@16.18.123)(jiti@2.4.2)(solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@16.18.123)))(solid-js@1.9.4)(yaml@2.7.0)
|
version: 5.0.2(@types/node@22.10.6)(jiti@2.4.2)(solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@22.10.6)))(solid-js@1.9.4)(yaml@2.7.0)
|
||||||
'@iconify-json/tabler':
|
'@iconify-json/tabler':
|
||||||
specifier: ^1.2.14
|
specifier: ^1.2.14
|
||||||
version: 1.2.14
|
version: 1.2.14
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)))
|
version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))
|
||||||
'@types/dateformat':
|
'@types/dateformat':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
@@ -198,6 +198,9 @@ importers:
|
|||||||
quicktype:
|
quicktype:
|
||||||
specifier: ^23.0.170
|
specifier: ^23.0.170
|
||||||
version: 23.0.170
|
version: 23.0.170
|
||||||
|
ts-node:
|
||||||
|
specifier: ^10.9.2
|
||||||
|
version: 10.9.2(@types/node@22.10.6)(typescript@5.7.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.7.3
|
version: 5.7.3
|
||||||
@@ -4209,9 +4212,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
trailbase@0.1.4:
|
|
||||||
resolution: {integrity: sha512-BYsd5ilurL3ht8zwQB9NTC+mVSzqx/o25oDbic0IlhAy30gQm+pAR4dT49CwojkpeQ3O/mRwtzlBZEzMtmNwEQ==}
|
|
||||||
|
|
||||||
trim-lines@3.0.1:
|
trim-lines@3.0.1:
|
||||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||||
|
|
||||||
@@ -4995,28 +4995,6 @@ snapshots:
|
|||||||
stream-replace-string: 2.0.0
|
stream-replace-string: 2.0.0
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
|
|
||||||
'@astrojs/solid-js@5.0.2(@types/node@16.18.123)(jiti@2.4.2)(solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@16.18.123)))(solid-js@1.9.4)(yaml@2.7.0)':
|
|
||||||
dependencies:
|
|
||||||
solid-js: 1.9.4
|
|
||||||
vite: 6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)
|
|
||||||
vite-plugin-solid: 2.11.0(solid-js@1.9.4)(vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0))
|
|
||||||
optionalDependencies:
|
|
||||||
solid-devtools: 0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@16.18.123))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@testing-library/jest-dom'
|
|
||||||
- '@types/node'
|
|
||||||
- jiti
|
|
||||||
- less
|
|
||||||
- lightningcss
|
|
||||||
- sass
|
|
||||||
- sass-embedded
|
|
||||||
- stylus
|
|
||||||
- sugarss
|
|
||||||
- supports-color
|
|
||||||
- terser
|
|
||||||
- tsx
|
|
||||||
- yaml
|
|
||||||
|
|
||||||
'@astrojs/solid-js@5.0.2(@types/node@22.10.6)(jiti@2.4.2)(solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@22.10.6)))(solid-js@1.9.4)(yaml@2.7.0)':
|
'@astrojs/solid-js@5.0.2(@types/node@22.10.6)(jiti@2.4.2)(solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@22.10.6)))(solid-js@1.9.4)(yaml@2.7.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
solid-js: 1.9.4
|
solid-js: 1.9.4
|
||||||
@@ -5097,16 +5075,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@astrojs/tailwind@5.1.4(astro@5.1.6(@types/node@16.18.123)(jiti@2.4.2)(rollup@4.30.1)(typescript@4.9.4)(yaml@2.7.0))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)))(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))':
|
|
||||||
dependencies:
|
|
||||||
astro: 5.1.6(@types/node@16.18.123)(jiti@2.4.2)(rollup@4.30.1)(typescript@4.9.4)(yaml@2.7.0)
|
|
||||||
autoprefixer: 10.4.20(postcss@8.5.1)
|
|
||||||
postcss: 8.5.1
|
|
||||||
postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
|
||||||
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- ts-node
|
|
||||||
|
|
||||||
'@astrojs/tailwind@5.1.4(astro@5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))':
|
'@astrojs/tailwind@5.1.4(astro@5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: 5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0)
|
astro: 5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0)
|
||||||
@@ -6168,14 +6136,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)))':
|
|
||||||
dependencies:
|
|
||||||
lodash.castarray: 4.4.0
|
|
||||||
lodash.isplainobject: 4.0.6
|
|
||||||
lodash.merge: 4.6.2
|
|
||||||
postcss-selector-parser: 6.0.10
|
|
||||||
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
|
||||||
|
|
||||||
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))':
|
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash.castarray: 4.4.0
|
lodash.castarray: 4.4.0
|
||||||
@@ -6600,103 +6560,6 @@ snapshots:
|
|||||||
valid-filename: 4.0.0
|
valid-filename: 4.0.0
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
|
|
||||||
astro@5.1.6(@types/node@16.18.123)(jiti@2.4.2)(rollup@4.30.1)(typescript@4.9.4)(yaml@2.7.0):
|
|
||||||
dependencies:
|
|
||||||
'@astrojs/compiler': 2.10.3
|
|
||||||
'@astrojs/internal-helpers': 0.4.2
|
|
||||||
'@astrojs/markdown-remark': 6.0.1
|
|
||||||
'@astrojs/telemetry': 3.2.0
|
|
||||||
'@oslojs/encoding': 1.1.0
|
|
||||||
'@rollup/pluginutils': 5.1.4(rollup@4.30.1)
|
|
||||||
'@types/cookie': 0.6.0
|
|
||||||
acorn: 8.14.0
|
|
||||||
aria-query: 5.3.2
|
|
||||||
axobject-query: 4.1.0
|
|
||||||
boxen: 8.0.1
|
|
||||||
ci-info: 4.1.0
|
|
||||||
clsx: 2.1.1
|
|
||||||
common-ancestor-path: 1.0.1
|
|
||||||
cookie: 0.7.2
|
|
||||||
cssesc: 3.0.0
|
|
||||||
debug: 4.4.0
|
|
||||||
deterministic-object-hash: 2.0.2
|
|
||||||
devalue: 5.1.1
|
|
||||||
diff: 5.2.0
|
|
||||||
dlv: 1.1.3
|
|
||||||
dset: 3.1.4
|
|
||||||
es-module-lexer: 1.6.0
|
|
||||||
esbuild: 0.21.5
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
fast-glob: 3.3.3
|
|
||||||
flattie: 1.1.1
|
|
||||||
github-slugger: 2.0.0
|
|
||||||
html-escaper: 3.0.3
|
|
||||||
http-cache-semantics: 4.1.1
|
|
||||||
js-yaml: 4.1.0
|
|
||||||
kleur: 4.1.5
|
|
||||||
magic-string: 0.30.17
|
|
||||||
magicast: 0.3.5
|
|
||||||
micromatch: 4.0.8
|
|
||||||
mrmime: 2.0.0
|
|
||||||
neotraverse: 0.6.18
|
|
||||||
p-limit: 6.2.0
|
|
||||||
p-queue: 8.0.1
|
|
||||||
preferred-pm: 4.0.0
|
|
||||||
prompts: 2.4.2
|
|
||||||
rehype: 13.0.2
|
|
||||||
semver: 7.6.3
|
|
||||||
shiki: 1.27.0
|
|
||||||
tinyexec: 0.3.2
|
|
||||||
tsconfck: 3.1.4(typescript@4.9.4)
|
|
||||||
ultrahtml: 1.5.3
|
|
||||||
unist-util-visit: 5.0.0
|
|
||||||
unstorage: 1.14.4
|
|
||||||
vfile: 6.0.3
|
|
||||||
vite: 6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)
|
|
||||||
vitefu: 1.0.5(vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0))
|
|
||||||
which-pm: 3.0.0
|
|
||||||
xxhash-wasm: 1.1.0
|
|
||||||
yargs-parser: 21.1.1
|
|
||||||
yocto-spinner: 0.1.2
|
|
||||||
zod: 3.24.1
|
|
||||||
zod-to-json-schema: 3.24.1(zod@3.24.1)
|
|
||||||
zod-to-ts: 1.2.0(typescript@4.9.4)(zod@3.24.1)
|
|
||||||
optionalDependencies:
|
|
||||||
sharp: 0.33.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@azure/app-configuration'
|
|
||||||
- '@azure/cosmos'
|
|
||||||
- '@azure/data-tables'
|
|
||||||
- '@azure/identity'
|
|
||||||
- '@azure/keyvault-secrets'
|
|
||||||
- '@azure/storage-blob'
|
|
||||||
- '@capacitor/preferences'
|
|
||||||
- '@deno/kv'
|
|
||||||
- '@netlify/blobs'
|
|
||||||
- '@planetscale/database'
|
|
||||||
- '@types/node'
|
|
||||||
- '@upstash/redis'
|
|
||||||
- '@vercel/blob'
|
|
||||||
- '@vercel/kv'
|
|
||||||
- aws4fetch
|
|
||||||
- db0
|
|
||||||
- idb-keyval
|
|
||||||
- ioredis
|
|
||||||
- jiti
|
|
||||||
- less
|
|
||||||
- lightningcss
|
|
||||||
- rollup
|
|
||||||
- sass
|
|
||||||
- sass-embedded
|
|
||||||
- stylus
|
|
||||||
- sugarss
|
|
||||||
- supports-color
|
|
||||||
- terser
|
|
||||||
- tsx
|
|
||||||
- typescript
|
|
||||||
- uploadthing
|
|
||||||
- yaml
|
|
||||||
|
|
||||||
astro@5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0):
|
astro@5.1.6(@types/node@22.10.6)(jiti@2.4.2)(rollup@4.30.1)(typescript@5.7.3)(yaml@2.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 2.10.3
|
'@astrojs/compiler': 2.10.3
|
||||||
@@ -8846,14 +8709,6 @@ snapshots:
|
|||||||
camelcase-css: 2.0.1
|
camelcase-css: 2.0.1
|
||||||
postcss: 8.5.1
|
postcss: 8.5.1
|
||||||
|
|
||||||
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)):
|
|
||||||
dependencies:
|
|
||||||
lilconfig: 3.1.3
|
|
||||||
yaml: 2.7.0
|
|
||||||
optionalDependencies:
|
|
||||||
postcss: 8.5.1
|
|
||||||
ts-node: 10.9.2(@types/node@16.18.123)(typescript@4.9.4)
|
|
||||||
|
|
||||||
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)):
|
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
@@ -9354,20 +9209,6 @@ snapshots:
|
|||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
sax: 1.4.1
|
sax: 1.4.1
|
||||||
|
|
||||||
solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@16.18.123)):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.26.0
|
|
||||||
'@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0)
|
|
||||||
'@babel/types': 7.26.5
|
|
||||||
'@solid-devtools/debugger': 0.23.4(solid-js@1.9.4)
|
|
||||||
'@solid-devtools/shared': 0.13.2(solid-js@1.9.4)
|
|
||||||
solid-js: 1.9.4
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 5.4.11(@types/node@16.18.123)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@22.10.6)):
|
solid-devtools@0.30.1(solid-js@1.9.4)(vite@5.4.11(@types/node@22.10.6)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
@@ -9552,33 +9393,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))
|
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3))
|
||||||
|
|
||||||
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4)):
|
|
||||||
dependencies:
|
|
||||||
'@alloc/quick-lru': 5.2.0
|
|
||||||
arg: 5.0.2
|
|
||||||
chokidar: 3.6.0
|
|
||||||
didyoumean: 1.2.2
|
|
||||||
dlv: 1.1.3
|
|
||||||
fast-glob: 3.3.3
|
|
||||||
glob-parent: 6.0.2
|
|
||||||
is-glob: 4.0.3
|
|
||||||
jiti: 1.21.7
|
|
||||||
lilconfig: 3.1.3
|
|
||||||
micromatch: 4.0.8
|
|
||||||
normalize-path: 3.0.0
|
|
||||||
object-hash: 3.0.0
|
|
||||||
picocolors: 1.1.1
|
|
||||||
postcss: 8.5.1
|
|
||||||
postcss-import: 15.1.0(postcss@8.5.1)
|
|
||||||
postcss-js: 4.0.1(postcss@8.5.1)
|
|
||||||
postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4))
|
|
||||||
postcss-nested: 6.2.0(postcss@8.5.1)
|
|
||||||
postcss-selector-parser: 6.1.2
|
|
||||||
resolve: 1.22.10
|
|
||||||
sucrase: 3.35.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- ts-node
|
|
||||||
|
|
||||||
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)):
|
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.6)(typescript@5.7.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
@@ -9657,11 +9471,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
trailbase@0.1.4:
|
|
||||||
dependencies:
|
|
||||||
jwt-decode: 4.0.0
|
|
||||||
uuid: 11.0.5
|
|
||||||
|
|
||||||
trim-lines@3.0.1: {}
|
trim-lines@3.0.1: {}
|
||||||
|
|
||||||
trough@2.2.0: {}
|
trough@2.2.0: {}
|
||||||
@@ -9707,7 +9516,6 @@ snapshots:
|
|||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
ts-poet@6.9.0:
|
ts-poet@6.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9724,10 +9532,6 @@ snapshots:
|
|||||||
ts-poet: 6.9.0
|
ts-poet: 6.9.0
|
||||||
ts-proto-descriptors: 2.0.0
|
ts-proto-descriptors: 2.0.0
|
||||||
|
|
||||||
tsconfck@3.1.4(typescript@4.9.4):
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 4.9.4
|
|
||||||
|
|
||||||
tsconfck@3.1.4(typescript@5.7.3):
|
tsconfck@3.1.4(typescript@5.7.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
@@ -9920,19 +9724,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.26.0
|
|
||||||
'@types/babel__core': 7.20.5
|
|
||||||
babel-preset-solid: 1.9.3(@babel/core@7.26.0)
|
|
||||||
merge-anything: 5.1.7
|
|
||||||
solid-js: 1.9.4
|
|
||||||
solid-refresh: 0.6.3(solid-js@1.9.4)
|
|
||||||
vite: 6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)
|
|
||||||
vitefu: 1.0.5(vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)):
|
vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
@@ -9957,16 +9748,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
vite@5.4.11(@types/node@16.18.123):
|
|
||||||
dependencies:
|
|
||||||
esbuild: 0.21.5
|
|
||||||
postcss: 8.5.1
|
|
||||||
rollup: 4.30.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 16.18.123
|
|
||||||
fsevents: 2.3.3
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
vite@5.4.11(@types/node@22.10.6):
|
vite@5.4.11(@types/node@22.10.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
@@ -9976,17 +9757,6 @@ snapshots:
|
|||||||
'@types/node': 22.10.6
|
'@types/node': 22.10.6
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0):
|
|
||||||
dependencies:
|
|
||||||
esbuild: 0.24.2
|
|
||||||
postcss: 8.5.1
|
|
||||||
rollup: 4.30.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 16.18.123
|
|
||||||
fsevents: 2.3.3
|
|
||||||
jiti: 2.4.2
|
|
||||||
yaml: 2.7.0
|
|
||||||
|
|
||||||
vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0):
|
vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.24.2
|
esbuild: 0.24.2
|
||||||
@@ -9998,10 +9768,6 @@ snapshots:
|
|||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
yaml: 2.7.0
|
yaml: 2.7.0
|
||||||
|
|
||||||
vitefu@1.0.5(vite@6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)):
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 6.0.7(@types/node@16.18.123)(jiti@2.4.2)(yaml@2.7.0)
|
|
||||||
|
|
||||||
vitefu@1.0.5(vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)):
|
vitefu@1.0.5(vite@6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)
|
vite: 6.0.7(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0)
|
||||||
@@ -10295,11 +10061,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
|
|
||||||
zod-to-ts@1.2.0(typescript@4.9.4)(zod@3.24.1):
|
|
||||||
dependencies:
|
|
||||||
typescript: 4.9.4
|
|
||||||
zod: 3.24.1
|
|
||||||
|
|
||||||
zod-to-ts@1.2.0(typescript@5.7.3)(zod@3.24.1):
|
zod-to-ts@1.2.0(typescript@5.7.3)(zod@3.24.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export type Pagination = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListResponse<T> = {
|
||||||
|
cursor?: string;
|
||||||
|
records: T[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Tokens = {
|
export type Tokens = {
|
||||||
auth_token: string;
|
auth_token: string;
|
||||||
refresh_token: string | null;
|
refresh_token: string | null;
|
||||||
@@ -120,7 +125,7 @@ export class RecordApi {
|
|||||||
pagination?: Pagination;
|
pagination?: Pagination;
|
||||||
order?: string[];
|
order?: string[];
|
||||||
filters?: string[];
|
filters?: string[];
|
||||||
}): Promise<T[]> {
|
}): Promise<ListResponse<T>> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const pagination = opts?.pagination;
|
const pagination = opts?.pagination;
|
||||||
if (pagination) {
|
if (pagination) {
|
||||||
@@ -149,7 +154,7 @@ export class RecordApi {
|
|||||||
const response = await this.client.fetch(
|
const response = await this.client.fetch(
|
||||||
`${RecordApi._recordApi}/${this.name}?${params}`,
|
`${RecordApi._recordApi}/${this.name}?${params}`,
|
||||||
);
|
);
|
||||||
return (await response.json()) as T[];
|
return (await response.json()) as ListResponse<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async read<T = Record<string, unknown>>(
|
public async read<T = Record<string, unknown>>(
|
||||||
|
|||||||
@@ -85,29 +85,33 @@ test("Record integration tests", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const records = await api.list<SimpleStrict>({
|
const response = await api.list<SimpleStrict>({
|
||||||
filters: [`text_not_null=${messages[0]}`],
|
filters: [`text_not_null=${messages[0]}`],
|
||||||
});
|
});
|
||||||
|
expect(response.cursor).not.undefined.and.not.toBe("");
|
||||||
|
const records = response.records;
|
||||||
expect(records.length).toBe(1);
|
expect(records.length).toBe(1);
|
||||||
expect(records[0].text_not_null).toBe(messages[0]);
|
expect(records[0].text_not_null).toBe(messages[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const records = await api.list<SimpleStrict>({
|
const response = await api.list<SimpleStrict>({
|
||||||
filters: [`text_not_null[like]=% =?&${now}`],
|
filters: [`text_not_null[like]=% =?&${now}`],
|
||||||
order: ["+text_not_null"],
|
order: ["+text_not_null"],
|
||||||
});
|
});
|
||||||
expect(records.map((el) => el.text_not_null)).toStrictEqual(messages);
|
expect(response.records.map((el) => el.text_not_null)).toStrictEqual(
|
||||||
|
messages,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const records = await api.list<SimpleStrict>({
|
const response = await api.list<SimpleStrict>({
|
||||||
filters: [`text_not_null[like]=%${now}`],
|
filters: [`text_not_null[like]=%${now}`],
|
||||||
order: ["-text_not_null"],
|
order: ["-text_not_null"],
|
||||||
});
|
});
|
||||||
expect(records.map((el) => el.text_not_null).reverse()).toStrictEqual(
|
expect(
|
||||||
messages,
|
response.records.map((el) => el.text_not_null).reverse(),
|
||||||
);
|
).toStrictEqual(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
const record: SimpleStrict = await api.read(ids[0]);
|
const record: SimpleStrict = await api.read(ids[0]);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use trailbase_sqlite::Value;
|
use trailbase_sqlite::Value;
|
||||||
|
|
||||||
@@ -13,6 +14,13 @@ use crate::listing::{
|
|||||||
};
|
};
|
||||||
use crate::records::sql_to_json::rows_to_json;
|
use crate::records::sql_to_json::rows_to_json;
|
||||||
use crate::records::{Permission, RecordError};
|
use crate::records::{Permission, RecordError};
|
||||||
|
use crate::util::uuid_to_b64;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ListResponse {
|
||||||
|
cursor: Option<String>,
|
||||||
|
records: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Lists records matching the given filters
|
/// Lists records matching the given filters
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -27,7 +35,7 @@ pub async fn list_records_handler(
|
|||||||
Path(api_name): Path<String>,
|
Path(api_name): Path<String>,
|
||||||
RawQuery(raw_url_query): RawQuery,
|
RawQuery(raw_url_query): RawQuery,
|
||||||
user: Option<User>,
|
user: Option<User>,
|
||||||
) -> Result<Json<serde_json::Value>, RecordError> {
|
) -> Result<Json<ListResponse>, RecordError> {
|
||||||
let Some(api) = state.lookup_record_api(&api_name) else {
|
let Some(api) = state.lookup_record_api(&api_name) else {
|
||||||
return Err(RecordError::ApiNotFound);
|
return Err(RecordError::ApiNotFound);
|
||||||
};
|
};
|
||||||
@@ -36,13 +44,17 @@ pub async fn list_records_handler(
|
|||||||
// on the table, i.e. no access -> empty results.
|
// on the table, i.e. no access -> empty results.
|
||||||
api.check_table_level_access(Permission::Read, user.as_ref())?;
|
api.check_table_level_access(Permission::Read, user.as_ref())?;
|
||||||
|
|
||||||
|
let metadata = api.metadata();
|
||||||
|
let Some((pk_index, _pk_column)) = metadata.record_pk_column() else {
|
||||||
|
return Err(RecordError::Internal("missing pk column".into()));
|
||||||
|
};
|
||||||
|
|
||||||
let (filter_params, cursor, limit, order) = match parse_query(raw_url_query) {
|
let (filter_params, cursor, limit, order) = match parse_query(raw_url_query) {
|
||||||
Some(q) => (Some(q.params), q.cursor, q.limit, q.order),
|
Some(q) => (Some(q.params), q.cursor, q.limit, q.order),
|
||||||
None => (None, None, None, None),
|
None => (None, None, None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Where clause contains column filters and cursor depending on what's present.
|
// Where clause contains column filters and cursor depending on what's present.
|
||||||
let metadata = api.metadata();
|
|
||||||
let WhereClause {
|
let WhereClause {
|
||||||
mut clause,
|
mut clause,
|
||||||
mut params,
|
mut params,
|
||||||
@@ -109,12 +121,28 @@ pub async fn list_records_handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let rows = state.conn().query(&query, params).await?;
|
let rows = state.conn().query(&query, params).await?;
|
||||||
|
let Some(last_row) = rows.last() else {
|
||||||
|
// Rows are empty:
|
||||||
|
return Ok(Json(ListResponse {
|
||||||
|
cursor: None,
|
||||||
|
records: vec![],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return Ok(Json(serde_json::Value::Array(
|
assert!(pk_index < last_row.len());
|
||||||
rows_to_json(metadata, rows, |col_name| !col_name.starts_with("_"))
|
let cursor = match &last_row[pk_index] {
|
||||||
.await
|
rusqlite::types::Value::Blob(blob) => {
|
||||||
.map_err(|err| RecordError::Internal(err.into()))?,
|
uuid::Uuid::from_slice(blob).as_ref().map(uuid_to_b64).ok()
|
||||||
)));
|
}
|
||||||
|
rusqlite::types::Value::Integer(i) => Some(i.to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let records = rows_to_json(metadata, rows, |col_name| !col_name.starts_with("_"))
|
||||||
|
.await
|
||||||
|
.map_err(|err| RecordError::Internal(err.into()))?;
|
||||||
|
|
||||||
|
return Ok(Json(ListResponse { cursor, records }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -261,7 +289,7 @@ mod tests {
|
|||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
) -> Result<Vec<serde_json::Value>, RecordError> {
|
) -> Result<Vec<serde_json::Value>, RecordError> {
|
||||||
let response = list_records_handler(
|
let json_response = list_records_handler(
|
||||||
State(state.clone()),
|
State(state.clone()),
|
||||||
Path("messages_api".to_string()),
|
Path("messages_api".to_string()),
|
||||||
RawQuery(query),
|
RawQuery(query),
|
||||||
@@ -269,10 +297,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let json = response.0;
|
let response: ListResponse = json_response.0;
|
||||||
if let serde_json::Value::Array(arr) = json {
|
return Ok(response.records);
|
||||||
return Ok(arr);
|
|
||||||
}
|
|
||||||
return Err(RecordError::BadRequest("Not a json array"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,23 +19,6 @@ pub enum JsonError {
|
|||||||
ValueNotFound,
|
ValueNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn value_to_json(value: rusqlite::types::Value) -> Result<serde_json::Value, JsonError> {
|
|
||||||
use rusqlite::types::Value;
|
|
||||||
|
|
||||||
return Ok(match value {
|
|
||||||
Value::Null => serde_json::Value::Null,
|
|
||||||
Value::Real(real) => {
|
|
||||||
let Some(number) = serde_json::Number::from_f64(real) else {
|
|
||||||
return Err(JsonError::Finite);
|
|
||||||
};
|
|
||||||
serde_json::Value::Number(number)
|
|
||||||
}
|
|
||||||
Value::Integer(integer) => serde_json::Value::Number(serde_json::Number::from(integer)),
|
|
||||||
Value::Blob(blob) => serde_json::Value::String(BASE64_URL_SAFE.encode(blob)),
|
|
||||||
Value::Text(text) => serde_json::Value::String(text),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn valueref_to_json(
|
pub(crate) fn valueref_to_json(
|
||||||
value: rusqlite::types::ValueRef<'_>,
|
value: rusqlite::types::ValueRef<'_>,
|
||||||
) -> Result<serde_json::Value, JsonError> {
|
) -> Result<serde_json::Value, JsonError> {
|
||||||
@@ -72,7 +55,7 @@ pub fn row_to_json(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = row.get_value(i).map_err(|_err| JsonError::ValueNotFound)?;
|
let value = row.get_value(i).ok_or(JsonError::ValueNotFound)?;
|
||||||
if let rusqlite::types::Value::Text(str) = &value {
|
if let rusqlite::types::Value::Text(str) = &value {
|
||||||
if let Some((_col, col_meta)) = metadata.column_by_name(col_name) {
|
if let Some((_col, col_meta)) = metadata.column_by_name(col_name) {
|
||||||
if col_meta.json.is_some() {
|
if col_meta.json.is_some() {
|
||||||
@@ -84,7 +67,7 @@ pub fn row_to_json(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.insert(col_name.to_string(), value_to_json(value)?);
|
map.insert(col_name.to_string(), valueref_to_json(value.into())?);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(serde_json::Value::Object(map));
|
return Ok(serde_json::Value::Object(map));
|
||||||
@@ -110,8 +93,8 @@ pub fn row_to_json_array(row: &trailbase_sqlite::Row) -> Result<Vec<serde_json::
|
|||||||
let mut json_row = Vec::<serde_json::Value>::with_capacity(cols);
|
let mut json_row = Vec::<serde_json::Value>::with_capacity(cols);
|
||||||
|
|
||||||
for i in 0..cols {
|
for i in 0..cols {
|
||||||
let value = row.get_value(i).map_err(|_err| JsonError::ValueNotFound)?;
|
let value = row.get_value(i).ok_or(JsonError::ValueNotFound)?;
|
||||||
json_row.push(value_to_json(value)?);
|
json_row.push(valueref_to_json(value.into())?);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(json_row);
|
return Ok(json_row);
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ pub trait TableOrViewMetadata {
|
|||||||
// Used by RecordAPI.
|
// Used by RecordAPI.
|
||||||
fn column_by_name(&self, key: &str) -> Option<(&Column, &ColumnMetadata)>;
|
fn column_by_name(&self, key: &str) -> Option<(&Column, &ColumnMetadata)>;
|
||||||
|
|
||||||
// Impl detail: only used by admin
|
// Impl detail: only used by admin and list
|
||||||
fn columns(&self) -> Option<Vec<Column>>;
|
fn columns(&self) -> Option<Vec<Column>>;
|
||||||
fn record_pk_column(&self) -> Option<(usize, &Column)>;
|
fn record_pk_column(&self) -> Option<(usize, &Column)>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use rusqlite::{types, Statement};
|
use rusqlite::{types, Statement};
|
||||||
use std::{fmt::Debug, str::FromStr, sync::Arc};
|
use std::fmt::Debug;
|
||||||
|
use std::ops::Index;
|
||||||
use crate::error::Error;
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum ValueType {
|
pub enum ValueType {
|
||||||
@@ -57,28 +58,39 @@ impl Rows {
|
|||||||
result.push(Row::from_row(row, Some(columns.clone()))?);
|
result.push(Row::from_row(row, Some(columns.clone()))?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(result, columns))
|
return Ok(Self(result, columns));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.0.len()
|
return self.0.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
return self.0.is_empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> std::slice::Iter<'_, Row> {
|
pub fn iter(&self) -> std::slice::Iter<'_, Row> {
|
||||||
self.0.iter()
|
return self.0.iter();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, idx: usize) -> Option<&Row> {
|
||||||
|
return self.0.get(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last(&self) -> Option<&Row> {
|
||||||
|
return self.0.last();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_count(&self) -> usize {
|
pub fn column_count(&self) -> usize {
|
||||||
self.1.len()
|
return self.1.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_names(&self) -> Vec<&str> {
|
pub fn column_names(&self) -> Vec<&str> {
|
||||||
self.1.iter().map(|s| s.name.as_str()).collect()
|
return self.1.iter().map(|s| s.name.as_str()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_name(&self, idx: usize) -> Option<&str> {
|
pub fn column_name(&self, idx: usize) -> Option<&str> {
|
||||||
self.1.get(idx).map(|c| c.name.as_str())
|
return self.1.get(idx).map(|c| c.name.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_type(&self, idx: usize) -> std::result::Result<ValueType, rusqlite::Error> {
|
pub fn column_type(&self, idx: usize) -> std::result::Result<ValueType, rusqlite::Error> {
|
||||||
@@ -91,6 +103,7 @@ impl Rows {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(rusqlite::Error::InvalidColumnType(
|
return Err(rusqlite::Error::InvalidColumnType(
|
||||||
idx,
|
idx,
|
||||||
self.column_name(idx).unwrap_or("?").to_string(),
|
self.column_name(idx).unwrap_or("?").to_string(),
|
||||||
@@ -99,6 +112,14 @@ impl Rows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Rows {
|
||||||
|
type Output = Row;
|
||||||
|
|
||||||
|
fn index(&self, idx: usize) -> &Self::Output {
|
||||||
|
return &self.0[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Row(Vec<types::Value>, Arc<Vec<Column>>);
|
pub struct Row(Vec<types::Value>, Arc<Vec<Column>>);
|
||||||
|
|
||||||
@@ -112,37 +133,50 @@ impl Row {
|
|||||||
values.push(row.get_ref(idx)?.into());
|
values.push(row.get_ref(idx)?.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(values, columns))
|
return Ok(Self(values, columns));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<T>(&self, idx: usize) -> types::FromSqlResult<T>
|
pub fn get<T>(&self, idx: usize) -> types::FromSqlResult<T>
|
||||||
where
|
where
|
||||||
T: types::FromSql,
|
T: types::FromSql,
|
||||||
{
|
{
|
||||||
let val = self
|
let Some(value) = self.0.get(idx) else {
|
||||||
.0
|
return Err(types::FromSqlError::OutOfRange(idx as i64));
|
||||||
.get(idx)
|
};
|
||||||
.ok_or_else(|| types::FromSqlError::Other("Index out of bounds".into()))?;
|
return T::column_result(value.into());
|
||||||
T::column_result(val.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_value(&self, idx: usize) -> Result<types::Value, Error> {
|
pub fn get_value(&self, idx: usize) -> Option<&types::Value> {
|
||||||
self
|
return self.0.get(idx);
|
||||||
.0
|
}
|
||||||
.get(idx)
|
|
||||||
.ok_or_else(|| Error::Other("Index out of bounds".into()))
|
pub fn len(&self) -> usize {
|
||||||
.cloned()
|
assert_eq!(self.1.len(), self.0.len());
|
||||||
|
return self.0.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
return self.0.is_empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_count(&self) -> usize {
|
pub fn column_count(&self) -> usize {
|
||||||
self.0.len()
|
assert_eq!(self.1.len(), self.0.len());
|
||||||
|
return self.1.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_names(&self) -> Vec<&str> {
|
pub fn column_names(&self) -> Vec<&str> {
|
||||||
self.1.iter().map(|s| s.name.as_str()).collect()
|
return self.1.iter().map(|s| s.name.as_str()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_name(&self, idx: usize) -> Option<&str> {
|
pub fn column_name(&self, idx: usize) -> Option<&str> {
|
||||||
self.1.get(idx).map(|c| c.name.as_str())
|
return self.1.get(idx).map(|c| c.name.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Row {
|
||||||
|
type Output = types::Value;
|
||||||
|
|
||||||
|
fn index(&self, idx: usize) -> &Self::Output {
|
||||||
|
return &self.0[idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user