Skip to content

Commit

Permalink
Allow for resolving related models (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
leo authored Feb 23, 2025
1 parent 9e9a256 commit 9f32095
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 9 deletions.
32 changes: 29 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import type {
Model as PrivateModel,
PublicModel,
} from '@/src/types/model';
import type { InternalStatement, Query, Statement } from '@/src/types/query';
import type {
AllQueryInstructions,
InternalStatement,
Query,
Statement,
} from '@/src/types/query';
import type {
ExpandedResult,
MultipleRecordResult,
Expand Down Expand Up @@ -96,10 +101,31 @@ class Transaction {
// If the model defined in the query is called `all`, that means we need to expand
// the query into multiple queries: One for each model.
if (queryModel === 'all') {
return modelsWithAttributes.map((model) => {
const { for: forInstruction, ...restInstructions } = (queryInstructions ||
{}) as AllQueryInstructions;

let modelList = modelsWithAttributes;

// If a `for` instruction was provided, that means we only want to select the
// related models of the model that was provided in `for`, instead of selecting
// all models at once.
if (forInstruction) {
const mainModel = getModelBySlug(modelsWithAttributes, forInstruction);

modelList = Object.values(mainModel.fields || {})
.filter((field) => field.type === 'link')
.map((field) => {
return modelsWithAttributes.find(
(model) => model.slug === field.target,
) as PrivateModel;
});
}

return modelList.map((model) => {
const query: Query = {
[queryType]: { [model.pluralSlug]: queryInstructions },
[queryType]: { [model.pluralSlug]: restInstructions },
};

return { query, expansionIndex };
});
}
Expand Down
4 changes: 3 additions & 1 deletion src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,9 @@ export const transformMetaQuery = (
let slug =
entity === 'model' && action === 'create'
? null
: (query[queryType]!.model as string);
: query[queryType] && 'model' in query[queryType]
? (query[queryType].model as string)
: null;
let modelSlug = slug;

let jsonValue: Record<string, unknown> | undefined;
Expand Down
18 changes: 13 additions & 5 deletions src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export type InstructionSchema =
| 'limitedTo'
| 'using';

// Query Types
// DML Query Types
export type GetQuery = Record<string, Omit<CombinedInstructions, 'to'> | null>;
export type SetQuery = Record<
string,
Expand All @@ -116,7 +116,14 @@ export type AddQuery = Record<
export type RemoveQuery = Record<string, Omit<CombinedInstructions, 'to'>>;
export type CountQuery = Record<string, Omit<CombinedInstructions, 'to'> | null>;

// Individual Instructions
// DML Query Types — Addressing all models
export type AllQueryInstructions = { for?: string };
export type AllQuery = { all: AllQueryInstructions | null };

export type GetAllQuery = AllQuery;
export type CountAllQuery = AllQuery;

// DML Query Types — Individual Instructions
export type GetInstructions = Omit<CombinedInstructions, 'to'>;
export type SetInstructions = Omit<CombinedInstructions, 'to'> & { to: FieldSelector };
export type AddInstructions = Omit<CombinedInstructions, 'with' | 'using'> & {
Expand All @@ -131,6 +138,7 @@ export type Instructions =
| RemoveInstructions
| CountInstructions;

// DDL Query Types - Individual Instructions
export type CreateQuery = {
model: string | PublicModel;
to?: PublicModel;
Expand Down Expand Up @@ -171,7 +179,7 @@ export type DropQuery = {
model: string;
};

// Model Queries
// DDL Query Types
export type ModelQuery =
| {
create: CreateQuery;
Expand All @@ -190,11 +198,11 @@ export type QueryPaginationOptions = {
};

export type Query = {
get?: GetQuery;
get?: GetQuery | GetAllQuery;
set?: SetQuery;
add?: AddQuery;
remove?: RemoveQuery;
count?: CountQuery;
count?: CountQuery | CountAllQuery;

create?: CreateQuery;
alter?: AlterQuery;
Expand Down
191 changes: 191 additions & 0 deletions tests/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,197 @@ test('get all records of all models', async () => {
});
});

test('get all records of all models with instructions', async () => {
const queries: Array<Query> = [
{
get: {
all: {
limitedTo: 1,
},
},
},
];

const models: Array<Model> = [
{
slug: 'account',
},
{
slug: 'team',
},
];

const transaction = new Transaction(queries, { models });

expect(transaction.statements).toEqual([
{
statement: `SELECT "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy" FROM "accounts" ORDER BY "ronin.createdAt" DESC LIMIT 2`,
params: [],
returning: true,
},
{
statement: `SELECT "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy" FROM "teams" ORDER BY "ronin.createdAt" DESC LIMIT 2`,
params: [],
returning: true,
},
]);

const rawResults = await queryEphemeralDatabase(models, transaction.statements);
const result = transaction.formatResults(rawResults)[0];

expect(result).toMatchObject({
models: {
accounts: {
records: [
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
],
modelFields: expect.objectContaining({
id: 'string',
}),
},
teams: {
records: [
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
],
modelFields: expect.objectContaining({
id: 'string',
}),
},
},
});
});

test('get all records of linked models', async () => {
const queries: Array<Query> = [
{
get: {
all: {
for: 'member',
},
},
},
];

const models: Array<Model> = [
{
slug: 'account',
},
{
slug: 'team',
},
{
slug: 'member',
fields: {
account: {
type: 'link',
target: 'account',
},
team: {
type: 'link',
target: 'team',
},
},
},
// These two should not end up in the final list of SQL statements. We are listing
// them here to ensure that they are correctly filtered out.
{
slug: 'beach',
},
{
slug: 'product',
},
];

const transaction = new Transaction(queries, { models });

expect(transaction.statements).toEqual([
{
statement: `SELECT "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy" FROM "accounts"`,
params: [],
returning: true,
},
{
statement: `SELECT "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy" FROM "teams"`,
params: [],
returning: true,
},
]);

const rawResults = await queryEphemeralDatabase(models, transaction.statements);
const result = transaction.formatResults(rawResults)[0];

expect(result).toMatchObject({
models: {
accounts: {
records: [
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
],
modelFields: expect.objectContaining({
id: 'string',
}),
},
teams: {
records: [
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
{
id: expect.stringMatching(RECORD_ID_REGEX),
ronin: {
createdAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
createdBy: null,
updatedAt: expect.stringMatching(RECORD_TIMESTAMP_REGEX),
updatedBy: null,
},
},
],
modelFields: expect.objectContaining({
id: 'string',
}),
},
},
});
});

test('count all records of all models', async () => {
const queries: Array<Query> = [
{
Expand Down

0 comments on commit 9f32095

Please sign in to comment.