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

Removed model formatting #7

Merged
merged 7 commits into from
Dec 20, 2024
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
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"dependencies": {
"@iarna/toml": "2.2.5",
"@inquirer/prompts": "7.2.0",
"@ronin/compiler": "0.12.4",
"@ronin/compiler": "0.12.6",
"@ronin/engine": "0.0.16",
"chalk-template": "1.1.0",
"get-port": "7.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/utils/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const printHelp = (): Promise<void> => {

login Authenticate with RONIN (run by default for every command)
init [space] Initialize the TypeScript types for a given space
migration create|apply Create or apply a migration for a locally defined database schema

{bold OPTIONS}

Expand Down
63 changes: 62 additions & 1 deletion src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import type { parseArgs } from 'node:util';
import { fieldsToCreate, fieldsToDrop } from '@/src/utils/field';
import type { Model } from '@ronin/compiler';
import type { Model, Result } from '@ronin/compiler';

/** Represents a data item for logging */
interface DataItem {
Expand Down Expand Up @@ -270,3 +270,64 @@ export const areArraysEqual = (arr1: Array<string>, arr2: Array<string>): boolea
return JSON.stringify(obj) === JSON.stringify(arr2[index]);
});
};

export type QueryResponse<T> = {
results: Array<Result<T>>;
error?: Error;
};

interface InvalidResponseErrorDetails {
message: string;
code: string;
}

export class InvalidResponseError extends Error {
message: InvalidResponseErrorDetails['message'];
code: InvalidResponseErrorDetails['code'];

constructor(details: InvalidResponseErrorDetails) {
super(details.message);

this.name = 'InvalidResponseError';
this.message = details.message;
this.code = details.code;
}
}

/**
* Parses the response as JSON or, alternatively, throws an error containing
* potential error details that might have been included in the response.
*
* @param response The response of a fetch request.
*
* @returns The response body as a JSON object.
*/
export const getResponseBody = async <T>(
response: Response,
options?: { errorPrefix?: string },
): Promise<T> => {
// If the response is okay, we want to parse the JSON asynchronously.
if (response.ok) return response.json() as T;

const text = await response.text();

let json: T & {
error?: InvalidResponseErrorDetails;
};

try {
json = JSON.parse(text);
} catch (_err) {
throw new InvalidResponseError({
message: `${options?.errorPrefix ? `${options.errorPrefix} ` : ''}${text}`,
code: 'JSON_PARSE_ERROR',
});
}

if (json.error) {
json.error.message = `${options?.errorPrefix ? `${options.errorPrefix} ` : ''}${json.error.message}`;
throw new InvalidResponseError(json.error);
}

return json;
};
102 changes: 34 additions & 68 deletions src/utils/model.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,9 @@
import { IGNORED_FIELDS } from '@/src/utils/migration';
import { type QueryResponse, getResponseBody } from '@/src/utils/misc';
import type { Model } from '@ronin/compiler';
import { Transaction } from '@ronin/compiler';
import type { Database } from '@ronin/engine';

interface Record {
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
indexes?: { [key: string]: any };
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
triggers?: { [key: string]: any };
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
fields?: { [key: string]: any };
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
presets?: { [key: string]: any };
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
[key: string]: any;
}

/**
* Formats a database record into a Model by transforming nested objects into arrays with slugs
*/
const formatRecord = (record: Record): Model => ({
...record,
slug: record.slug,
indexes: Object.entries(record.indexes || {}).map(([slug, value]) => ({
slug,
...JSON.parse(JSON.stringify(value)),
})),
presets: Object.entries(record.presets || {}).map(([slug, preset]) => ({
...preset,
slug,
})),
triggers: Object.entries(record.triggers || {}).map(([slug, value]) => ({
slug,
...JSON.parse(value),
})),
fields: Object.entries(record.fields || {})
.filter(([slug]) => !IGNORED_FIELDS.includes(slug))
.map(([slug, value]) => ({
...(typeof value === 'string' ? JSON.parse(value) : value),
slug,
})),
});
import type { Row } from '@ronin/engine/types';

/**
* Fetches and formats schema models from either production API or local database.
Expand All @@ -61,39 +24,42 @@ export const getModels = async (
isProduction?: boolean,
): Promise<Array<Model>> => {
const transaction = new Transaction([{ get: { models: null } }]);
const statements = transaction.statements.map((s) => s.statement);

if (!isProduction) {
const rawResult = await db.query(statements);
const result = transaction.formatResults(
rawResult.map((r) => r.rows),
false,
);
// biome-ignore lint/suspicious/noExplicitAny: These will be inferred shortly.
const records = (result[0] as any).records as Array<Record>;
const formatted = records.map(formatRecord);
let rawResults: Array<Array<Row>>;

return formatted;
}
if (isProduction) {
try {
const nativeQueries = transaction.statements.map((statement) => ({
query: statement.statement,
values: statement.params,
}));

try {
const response = await fetch(`https://data.ronin.co/?data-selector=${spaceId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
nativeQueries: statements.map((query) => ({ query })),
}),
});
const response = await fetch(`https://data.ronin.co/?data-selector=${spaceId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ nativeQueries }),
});

const { results } = (await response.json()) as {
results: Array<{ records: Array<Record> }>;
};
const responseResults = await getResponseBody<QueryResponse<Model>>(response);

return results[0].records.map(formatRecord);
} catch (error) {
throw new Error(`Failed to fetch remote models: ${(error as Error).message}`);
rawResults = responseResults.results.map((result) => {
return 'records' in result ? result.records : [];
});
} catch (error) {
throw new Error(`Failed to fetch remote models: ${(error as Error).message}`);
}
} else {
rawResults = (await db.query(transaction.statements)).map((r) => r.rows);
}

const results = transaction.formatResults<Model>(rawResults, false);
const models = 'records' in results[0] ? results[0].records : [];

return models.map((model) => ({
...model,
fields: model.fields?.filter((field) => !IGNORED_FIELDS.includes(field.slug)),
}));
};
7 changes: 5 additions & 2 deletions tests/fixtures/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const engine = new Engine({
*
* @returns A list of rows resulting from the executed statements.
*/
export const prefilledDatabase = async (models: Array<Model>): Promise<Database> => {
export const queryEphemeralDatabase = async (models: Array<Model>): Promise<Database> => {
const databaseName = Math.random().toString(36).substring(7);
const db = await engine.createDatabase({ id: databaseName });

Expand All @@ -38,7 +38,10 @@ export const prefilledDatabase = async (models: Array<Model>): Promise<Database>
* @param databaseName - The name of the database to prefill.
* @param models - The models that should be inserted into the database.
*/
export const prefillDatabase = async (db: Database, models: Array<Model>) => {
export const prefillDatabase = async (
db: Database,
models: Array<Model>,
): Promise<void> => {
const queries = models.map((model) => {
return {
create: {
Expand Down
16 changes: 8 additions & 8 deletions tests/utils/apply.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Account, AccountNew, TestA, TestB, TestF } from '@/fixtures/index';

import { describe, expect, test } from 'bun:test';
import { prefilledDatabase } from '@/fixtures/utils';
import { queryEphemeralDatabase } from '@/fixtures/utils';
import { diffModels } from '@/src/utils/migration';
import { getModels } from '@/src/utils/model';
import { Protocol } from '@/src/utils/protocol';
Expand All @@ -16,10 +16,10 @@ describe('apply', () => {
const protocol = new Protocol(modelDiff);
await protocol.convertToQueryObjects();

const statements = await protocol.getSQLStatements(existingModels);
const statements = protocol.getSQLStatements(existingModels);
expect(statements).toHaveLength(4);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);
await db.query(statements);

expect(db).toBeDefined();
Expand All @@ -41,7 +41,7 @@ describe('apply', () => {
const statements = protocol.getSQLStatements(existingModels);
expect(statements).toHaveLength(2);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);
await db.query(statements);

expect(db).toBeDefined();
Expand All @@ -62,7 +62,7 @@ describe('apply', () => {
const statements = protocol.getSQLStatements(existingModels);
expect(statements).toHaveLength(4);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);
await db.query(statements);

expect(db).toBeDefined();
Expand All @@ -84,7 +84,7 @@ describe('apply', () => {
const statements = protocol.getSQLStatements(existingModels);
expect(statements).toHaveLength(2);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);
await db.query(statements);

expect(db).toBeDefined();
Expand All @@ -106,7 +106,7 @@ describe('apply', () => {

expect(statements).toHaveLength(9);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);

expect(db).toBeDefined();

Expand All @@ -130,7 +130,7 @@ describe('apply', () => {

expect(statements).toHaveLength(2);

const db = await prefilledDatabase(existingModels);
const db = await queryEphemeralDatabase(existingModels);

expect(db).toBeDefined();

Expand Down
18 changes: 18 additions & 0 deletions tests/utils/misc.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { afterEach, describe, expect, mock, spyOn, test } from 'bun:test';

import {
InvalidResponseError,
MODEL_IN_CODE_PATH,
areArraysEqual,
getModelDefinitions,
getResponseBody,
logDataTable,
logTableDiff,
sortModels,
Expand Down Expand Up @@ -306,3 +308,19 @@ describe('areArraysEqual', () => {
});
});
});

describe('getResponseBody', () => {
test('get response with broken body', async () => {
const response = new Response('test', { status: 400 });

let error: Error | undefined;

try {
await getResponseBody(response);
} catch (err) {
error = err as Error;
}

expect(error).toBeInstanceOf(InvalidResponseError);
});
});
2 changes: 1 addition & 1 deletion tests/utils/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('models', () => {
expect(models).toHaveLength(0);
});

test('Get models fails', async () => {
test('get models fails', async () => {
mock('https://ronin.co/api', {
response: {
status: 500,
Expand Down
Loading