Skip to content

Commit

Permalink
Ensure correct record metadata (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
leo authored Feb 21, 2025
1 parent 6db17a4 commit 161796e
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 33 deletions.
35 changes: 20 additions & 15 deletions src/instructions/to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getFieldFromModel,
getModelBySlug,
} from '@/src/model';
import { getRecordIdentifier } from '@/src/model/defaults';
import type { Model } from '@/src/types/model';
import type {
FieldValue,
Expand All @@ -11,7 +12,6 @@ import type {
} from '@/src/types/query';
import {
CURRENT_TIME_EXPRESSION,
ID_EXPRESSION,
flatten,
getQuerySymbol,
isObject,
Expand Down Expand Up @@ -50,26 +50,31 @@ export const handleTo = (
): string => {
const { with: withInstruction, to: toInstruction } = instructions;
const defaultFields: Record<string, unknown> = {};

const inlineDefaultInsertionFields = queryType === 'add' && options?.inlineDefaults;
const currentTime = new Date().toISOString();

// If records are being created, assign a default ID to them, unless a custom ID was
// already provided in the query.
if (inlineDefaultInsertionFields) {
defaultFields.id = toInstruction.id || ID_EXPRESSION(model.idPrefix);
if (queryType === 'add' && options?.inlineDefaults) {
defaultFields.id = toInstruction.id || getRecordIdentifier(model.idPrefix);
}

if (queryType === 'add' || queryType === 'set' || toInstruction.ronin) {
const defaults = {
// If records are being created, set their creation time.
...(inlineDefaultInsertionFields ? { createdAt: CURRENT_TIME_EXPRESSION } : {}),
// If records are being updated, bump their update time.
...(queryType === 'set' || inlineDefaultInsertionFields
? { updatedAt: CURRENT_TIME_EXPRESSION }
: {}),
// Allow for overwriting the default values provided above.
...(toInstruction.ronin as object),
};
const defaults = options?.inlineDefaults
? {
// If records are being created, set their creation time.
...(queryType === 'add' && { createdAt: currentTime }),
// If records are being updated or created, bump their update time.
updatedAt: currentTime,
// Allow for overwriting the default values provided above.
...(toInstruction.ronin as object),
}
: {
// If records are being updated, bump their update time.
// The creation time is already set using a default value in the DB.
...(queryType === 'set' ? { updatedAt: CURRENT_TIME_EXPRESSION } : {}),
// Allow for overwriting the default values provided above.
...(toInstruction.ronin as object),
};

if (Object.keys(defaults).length > 0) defaultFields.ronin = defaults;
}
Expand Down
8 changes: 4 additions & 4 deletions src/model/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ const modelAttributes: Array<
];

/**
* Generates a unique identifier for a newly created model.
* Generates a unique identifier for a newly created record.
*
* @returns A string containing the ID.
*/
const getModelIdentifier = (): string => {
return `mod_${Array.from(crypto.getRandomValues(new Uint8Array(12)))
export const getRecordIdentifier = (prefix: string): string => {
return `${prefix}_${Array.from(crypto.getRandomValues(new Uint8Array(12)))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.slice(0, 16)
Expand All @@ -132,7 +132,7 @@ export const addDefaultModelAttributes = (model: PartialModel, isNew: boolean):
// Generate a unique identifier for the model. We are generating these identifiers
// within the compiler instead of the database because the compiler needs it for
// internal comparisons, before the resulting statements hit the database.
if (isNew && !copiedModel.id) copiedModel.id = getModelIdentifier();
if (isNew && !copiedModel.id) copiedModel.id = getRecordIdentifier('mod');

for (const [setting, base, generator, mustRegenerate] of modelAttributes) {
// If an existing model is being altered, check whether the attribute must even be
Expand Down
8 changes: 6 additions & 2 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import type {
} from '@/src/types/query';
import {
CURRENT_TIME_EXPRESSION,
ID_EXPRESSION,
MODEL_ENTITY_ERROR_CODES,
QUERY_SYMBOLS,
RoninError,
Expand Down Expand Up @@ -218,7 +217,12 @@ export const getSystemFields = (idPrefix: Model['idPrefix']): Model['fields'] =>
id: {
name: 'ID',
type: 'string',
defaultValue: ID_EXPRESSION(idPrefix),
defaultValue: {
// Since default values in SQLite cannot rely on other columns, we unfortunately
// cannot rely on the `idPrefix` column here. Instead, we need to inject it
// directly into the expression as a static string.
[QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`,
},
},
'ronin.createdAt': {
name: 'RONIN - Created At',
Expand Down
9 changes: 0 additions & 9 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,6 @@ export const CURRENT_TIME_EXPRESSION = {
[QUERY_SYMBOLS.EXPRESSION]: `strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z'`,
};

export const ID_EXPRESSION = (
idPrefix: string,
): Record<typeof QUERY_SYMBOLS.EXPRESSION, string> => ({
// Since default values in SQLite cannot rely on other columns, we unfortunately
// cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
// into the expression as a static string.
[QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`,
});

// A regular expression for splitting up the components of a field mounting path, meaning
// the path within a record under which a particular field's value should be mounted.
const MOUNTING_PATH_SUFFIX = /(.*?)(\{(\d+)\})?$/;
Expand Down
16 changes: 13 additions & 3 deletions tests/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from 'bun:test';
import { queryEphemeralDatabase } from '@/fixtures/utils';
import {
RECORD_ID_REGEX,
RECORD_TIMESTAMP_REGEX,
queryEphemeralDatabase,
} from '@/fixtures/utils';
import { type Model, type Query, Transaction } from '@/src/index';
import { getSystemFields } from '@/src/model';
import type { SingleRecordResult } from '@/src/types/result';
Expand Down Expand Up @@ -184,8 +188,14 @@ test('inline default values', async () => {

expect(transaction.statements).toEqual([
{
statement: `INSERT INTO "accounts" ("handle", "emails", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, ?2, 'acc_' || lower(substr(hex(randomblob(12)), 1, 16)), strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z', strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z') RETURNING "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy", "handle", "emails"`,
params: ['elaine', '["[email protected]","[email protected]"]'],
statement: `INSERT INTO "accounts" ("handle", "emails", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, ?2, ?3, ?4, ?5) RETURNING "id", "ronin.createdAt", "ronin.createdBy", "ronin.updatedAt", "ronin.updatedBy", "handle", "emails"`,
params: [
'elaine',
'["[email protected]","[email protected]"]',
expect.stringMatching(RECORD_ID_REGEX),
expect.stringMatching(RECORD_TIMESTAMP_REGEX),
expect.stringMatching(RECORD_TIMESTAMP_REGEX),
],
returning: true,
},
]);
Expand Down

0 comments on commit 161796e

Please sign in to comment.