Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for resolving related models #152

Merged
merged 4 commits into from
Feb 23, 2025
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
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
Loading