Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const inference = {
check("columns no decorator names are typed by their rows", JSON.stringify(kinds(fieldsOf("shop", "customers"))) === JSON.stringify(["_id:string", "email:string", "company:string", "credit_limit:number", "vip:boolean"]), kinds(fieldsOf("shop", "customers"))),
check("an array is typed by its elements", JSON.stringify(pick(fieldsOf("shop", "products"), "tags")) === JSON.stringify({ kind: "array", element: { kind: "string" } }), pick(fieldsOf("shop", "products"), "tags")),
check("an array of no element stays untyped", JSON.stringify(pick(fieldsOf("demo", "posts"), "tag_ids")) === JSON.stringify({ kind: "array" }), pick(fieldsOf("demo", "posts"), "tag_ids")),
check("tables of named instances are sampled from the first instance holding rows", Object.keys(fieldsOf("shop", "order_items") ?? {}).join() === "_id,order_id,product_id,quantity,unit_price", fieldsOf("shop", "order_items")),
check("the columns of a table whose rows are all in named instances are sampled from them", Object.keys(fieldsOf("shop", "order_items") ?? {}).join() === "_id,order_id,product_id,quantity,unit_price", fieldsOf("shop", "order_items")),
check("the instance tag of the store is not a column", ![...Object.keys(orders ?? {}), ...Object.keys(fieldsOf("shop", "customers") ?? {})].includes("_instance"), Object.keys(orders ?? {})),
check("a column named constructor is a column of its own", JSON.stringify(Object.keys(members ?? {})) === JSON.stringify(["_id", "constructor", "note"]), Object.keys(members ?? {})),
check("a schema reports its instances and stats", JSON.stringify(pick(shop, "instances")) === '["eu","us"]' && JSON.stringify(pick(shop, "stats")) === JSON.stringify({ tableCount: 4, elementCount: 6, instanceCount: 3 }), pick(shop, "stats")),
Expand Down
14 changes: 5 additions & 9 deletions src/service/introspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function inferFieldType(value: unknown): FieldDescriptor | undefined {
async function sampleRows(
schema: Schema,
tableName: string,
instance?: string,
instance?: InstanceId,
): Promise<unknown[]> {
try {
const rows: unknown[] = await schema
Expand All @@ -187,21 +187,17 @@ async function sampleRows(

/**
* The rows to infer a table's columns from: the default instance's, or, when
* it holds none, the first named instance holding some. A schema whose data
* lives in named instances (one per region, per tenant) otherwise shows only
* its decorated columns.
* it holds none, rows read across every instance in one query. A schema whose
* data lives in named instances (one per region, per tenant) otherwise shows
* only its decorated columns.
*/
async function sampleAnyInstance(
schema: Schema,
tableName: string,
): Promise<unknown[]> {
const rows = await sampleRows(schema, tableName);
if (rows.length > 0) return rows;
for (const instance of await listSchemaInstances(schema)) {
const named = await sampleRows(schema, tableName, instance);
if (named.length > 0) return named;
}
return [];
return sampleRows(schema, tableName, CROSS_INSTANCE);
}

/**
Expand Down
50 changes: 47 additions & 3 deletions src/test/schemas-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,18 @@ class SchemasPageInvoice extends Table {
declare coupon_id: string | null;
}

/** Keeps the decorated class referenced: the decorator registers it. */
const TABLES = [SchemasPageInvoice];
/** A schema whose rows all live in a named instance, one per tenant. */
const TENANTED = "schemas-page-tenanted";
const TENANT = "tenant-1";

@RegisterTable("credit_notes", TENANTED)
class SchemasPageCreditNote extends Table {
declare _id: string;
declare amount: number;
}

/** Keeps the decorated classes referenced: the decorator registers them. */
const TABLES = [SchemasPageInvoice, SchemasPageCreditNote];

function shop() {
const schema = Schema.get<{ invoices: SchemasPageInvoice }>(SCHEMA);
Expand All @@ -43,7 +53,7 @@ function shop() {
}

async function seed(): Promise<void> {
assert.equal(TABLES.length, 1);
assert.equal(TABLES.length, 2);
await RegisterSchema(SCHEMA);
await shop()
.instance()
Expand Down Expand Up @@ -157,6 +167,40 @@ describe("[integration] Schemas page", () => {
});
});

describe("[integration] Schemas page on a tenanted schema", () => {
function tenanted() {
const schema = Schema.get<{ credit_notes: SchemasPageCreditNote }>(
TENANTED,
);
assert.ok(schema, "the tenanted schema is registered");
return schema;
}

before(async () => {
await RegisterSchema(TENANTED);
await tenanted()
.createInstance(TENANT)
.run()
.catch(() => undefined);
await tenanted()
.instance(TENANT)
.table("credit_notes")
.insert([{ _id: "cn-1", amount: 5 }])
.run();
});
after(() => tenanted().destroyInstance(TENANT).run());

it("types the columns of a table whose rows are all in a named instance", async () => {
const summary = (await listSchemaSummaries()).find(
(item) => item.id === TENANTED,
);
const fields = summary?.tables.find(
(table) => table.name === "credit_notes",
)?.fields;
assert.deepEqual(fields?.amount, { kind: "number" });
});
});

describe("Schemas filter bar parameters", () => {
it("reads the default instance, every instance and named ones", () => {
assert.deepEqual(decodeInstanceParam(undefined), { kind: "default" });
Expand Down
Loading