@ronin/compiler 0.8.6 → 0.8.8-leo-ron-1083-experimental-191
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +52 -18
- package/dist/index.d.ts +41 -13
- package/dist/index.js +121 -60
- package/package.json +2 -1
package/README.md
CHANGED
@@ -39,30 +39,49 @@ You will just need to make sure that, once you [create a pull request](https://d
|
|
39
39
|
The programmatic API of the RONIN compiler looks like this:
|
40
40
|
|
41
41
|
```typescript
|
42
|
-
import {
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
const queries: Array<Query> = [{
|
51
|
-
get: {
|
52
|
-
accounts: null
|
42
|
+
import { Transaction } from '@ronin/compiler';
|
43
|
+
|
44
|
+
const transaction = new Transaction([
|
45
|
+
{
|
46
|
+
create: { model: { slug: 'account' } }
|
47
|
+
},
|
48
|
+
{
|
49
|
+
get: { accounts: null }
|
53
50
|
}
|
54
|
-
|
51
|
+
]);
|
55
52
|
|
56
|
-
|
57
|
-
slug: 'account'
|
58
|
-
}];
|
59
|
-
|
60
|
-
const statements: Array<Statement> = compileQueries(queries, models);
|
53
|
+
transaction.statements;
|
61
54
|
// [{
|
62
55
|
// statement: 'SELECT * FROM "accounts"',
|
63
56
|
// params: [],
|
64
57
|
// returning: true,
|
65
58
|
// }]
|
59
|
+
|
60
|
+
transaction.models;
|
61
|
+
// [{
|
62
|
+
// name: 'Account',
|
63
|
+
// slug: 'account'
|
64
|
+
// ...
|
65
|
+
// }]
|
66
|
+
```
|
67
|
+
|
68
|
+
#### Types
|
69
|
+
|
70
|
+
In total, the following types are being exported:
|
71
|
+
|
72
|
+
```typescript
|
73
|
+
import type {
|
74
|
+
Query,
|
75
|
+
|
76
|
+
Model,
|
77
|
+
ModelField,
|
78
|
+
ModelIndex,
|
79
|
+
ModelTrigger,
|
80
|
+
ModelPreset,
|
81
|
+
|
82
|
+
Statement,
|
83
|
+
Result
|
84
|
+
} from '@ronin/compiler';
|
66
85
|
```
|
67
86
|
|
68
87
|
#### Options
|
@@ -70,7 +89,12 @@ const statements: Array<Statement> = compileQueries(queries, models);
|
|
70
89
|
To fine-tune the behavior of the compiler, you can pass the following options:
|
71
90
|
|
72
91
|
```typescript
|
73
|
-
|
92
|
+
new Transaction(queries, {
|
93
|
+
// A list of models that already existing inside the database.
|
94
|
+
models: [
|
95
|
+
{ slug: 'account' }
|
96
|
+
],
|
97
|
+
|
74
98
|
// Instead of returning an array of parameters for every statement (which allows for
|
75
99
|
// preventing SQL injections), all parameters are inlined directly into the SQL strings.
|
76
100
|
// This option should only be used if the generated SQL will be manually verified.
|
@@ -88,6 +112,16 @@ bun run dev
|
|
88
112
|
|
89
113
|
Whenever you make a change to the source code, it will then automatically be transpiled again.
|
90
114
|
|
115
|
+
### Mental Model
|
116
|
+
|
117
|
+
The interface of creating new `Transaction` instances (thereby creating new transactions) was chosen in order to define the smallest workload unit that the compiler can operate on.
|
118
|
+
|
119
|
+
Just like for the database, a transaction for the compiler defines an [atomic operation](https://www.sqlite.org/lang_transaction.html) in which a list of queries can be executed serially, and where each query can rely on the changes made by a previous one. In order to facilitate this, a programmatic interface that clarifies the accumulation of in-memory state is required (class instances).
|
120
|
+
|
121
|
+
For example, if one query creates a new model, every query after it within the same transaction must be able to interact with the records of that model, or update the model itself, without roundtrips to the database, thereby requiring the accumulation of state while the transaction is being compiled.
|
122
|
+
|
123
|
+
Furthermore, since every database transaction causes a [lock](https://www.sqlite.org/lockingv3.html), the database is inherently not locked between the execution of multiple transactions, which could cause the compilation inputs (e.g. models) of a `Transaction` to no longer be up-to-date. If the inputs have changed, a `new Transaction` should therefore be created.
|
124
|
+
|
91
125
|
### Running Tests
|
92
126
|
|
93
127
|
The RONIN compiler has 100% test coverage, which means that every single line of code is tested automatically, to ensure that any change to the source code doesn't cause a regression.
|
package/dist/index.d.ts
CHANGED
@@ -5972,17 +5972,45 @@ type PublicModel<T extends Array<ModelField> = Array<ModelField>> = Omit<Partial
|
|
5972
5972
|
identifiers?: Partial<Model['identifiers']>;
|
5973
5973
|
};
|
5974
5974
|
|
5975
|
-
|
5976
|
-
|
5977
|
-
|
5978
|
-
|
5979
|
-
|
5980
|
-
|
5981
|
-
|
5982
|
-
|
5983
|
-
|
5984
|
-
|
5985
|
-
|
5986
|
-
|
5975
|
+
type Row = Record<string, unknown>;
|
5976
|
+
type NativeRecord = Record<string, unknown> & {
|
5977
|
+
id: string;
|
5978
|
+
ronin: {
|
5979
|
+
createdAt: Date;
|
5980
|
+
updatedAt: Date;
|
5981
|
+
};
|
5982
|
+
};
|
5983
|
+
type SingleRecordResult = {
|
5984
|
+
record: NativeRecord | null;
|
5985
|
+
};
|
5986
|
+
type MultipleRecordResult = {
|
5987
|
+
records: Array<NativeRecord>;
|
5988
|
+
moreAfter?: string;
|
5989
|
+
moreBefore?: string;
|
5990
|
+
};
|
5991
|
+
type AmountResult = {
|
5992
|
+
amount: number;
|
5993
|
+
};
|
5994
|
+
type Result = SingleRecordResult | MultipleRecordResult | AmountResult;
|
5995
|
+
|
5996
|
+
declare class Transaction {
|
5997
|
+
statements: Array<Statement>;
|
5998
|
+
models: Array<PublicModel>;
|
5999
|
+
private queries;
|
6000
|
+
constructor(queries: Array<Query>, options?: Parameters<typeof this.compileQueries>[2] & {
|
6001
|
+
models?: Array<PublicModel>;
|
6002
|
+
});
|
6003
|
+
/**
|
6004
|
+
* Composes SQL statements for the provided RONIN queries.
|
6005
|
+
*
|
6006
|
+
* @param queries - The RONIN queries for which SQL statements should be composed.
|
6007
|
+
* @param models - A list of models.
|
6008
|
+
* @param options - Additional options to adjust the behavior of the statement generation.
|
6009
|
+
*
|
6010
|
+
* @returns The composed SQL statements.
|
6011
|
+
*/
|
6012
|
+
private compileQueries;
|
6013
|
+
formatOutput(results: Array<Array<Row>>): Array<Result>;
|
6014
|
+
}
|
5987
6015
|
|
5988
|
-
export { type PublicModel as Model, type ModelField, type ModelIndex, type ModelPreset, type ModelTrigger, type Query, type Statement,
|
6016
|
+
export { type PublicModel as Model, type ModelField, type ModelIndex, type ModelPreset, type ModelTrigger, type Query, type Result, type Statement, Transaction };
|
package/dist/index.js
CHANGED
@@ -145,7 +145,8 @@ var composeFieldValues = (models, model, statementParams, instructionName, value
|
|
145
145
|
);
|
146
146
|
const collectStatementValue = options.type !== "fields";
|
147
147
|
const symbol = getSymbol(value);
|
148
|
-
|
148
|
+
const syntax = WITH_CONDITIONS[options.condition || "being"](value);
|
149
|
+
let conditionValue = syntax[1];
|
149
150
|
if (symbol) {
|
150
151
|
if (symbol?.type === "expression") {
|
151
152
|
conditionValue = parseFieldExpression(
|
@@ -159,11 +160,11 @@ var composeFieldValues = (models, model, statementParams, instructionName, value
|
|
159
160
|
conditionValue = `(${compileQueryInput(symbol.value, models, statementParams).main.statement})`;
|
160
161
|
}
|
161
162
|
} else if (collectStatementValue) {
|
162
|
-
conditionValue = prepareStatementValue(statementParams,
|
163
|
+
conditionValue = prepareStatementValue(statementParams, conditionValue);
|
163
164
|
}
|
164
165
|
if (options.type === "fields") return conditionSelector;
|
165
166
|
if (options.type === "values") return conditionValue;
|
166
|
-
return `${conditionSelector} ${
|
167
|
+
return `${conditionSelector} ${syntax[0]} ${conditionValue}`;
|
167
168
|
};
|
168
169
|
var composeConditions = (models, model, statementParams, instructionName, value, options) => {
|
169
170
|
const isNested = isObject(value) && Object.keys(value).length > 0;
|
@@ -293,18 +294,18 @@ var getMatcher = (value, negative) => {
|
|
293
294
|
return "=";
|
294
295
|
};
|
295
296
|
var WITH_CONDITIONS = {
|
296
|
-
being: (value
|
297
|
-
notBeing: (value
|
298
|
-
startingWith: (value) =>
|
299
|
-
notStartingWith: (value) =>
|
300
|
-
endingWith: (value) =>
|
301
|
-
notEndingWith: (value) =>
|
302
|
-
containing: (value) =>
|
303
|
-
notContaining: (value) =>
|
304
|
-
greaterThan: (value) =>
|
305
|
-
greaterOrEqual: (value) =>
|
306
|
-
lessThan: (value) =>
|
307
|
-
lessOrEqual: (value) =>
|
297
|
+
being: (value) => [getMatcher(value, false), value],
|
298
|
+
notBeing: (value) => [getMatcher(value, true), value],
|
299
|
+
startingWith: (value) => ["LIKE", `${value}%`],
|
300
|
+
notStartingWith: (value) => ["NOT LIKE", `${value}%`],
|
301
|
+
endingWith: (value) => ["LIKE", `%${value}`],
|
302
|
+
notEndingWith: (value) => ["NOT LIKE", `%${value}`],
|
303
|
+
containing: (value) => ["LIKE", `%${value}%`],
|
304
|
+
notContaining: (value) => ["NOT LIKE", `%${value}%`],
|
305
|
+
greaterThan: (value) => [">", value],
|
306
|
+
greaterOrEqual: (value) => [">=", value],
|
307
|
+
lessThan: (value) => ["<", value],
|
308
|
+
lessOrEqual: (value) => ["<=", value]
|
308
309
|
};
|
309
310
|
var handleWith = (models, model, statementParams, instruction, parentModel) => {
|
310
311
|
const subStatement = composeConditions(
|
@@ -633,6 +634,13 @@ var PLURAL_MODEL_ENTITIES = {
|
|
633
634
|
trigger: "triggers",
|
634
635
|
preset: "presets"
|
635
636
|
};
|
637
|
+
var formatModelEntity = (type, entities) => {
|
638
|
+
const entries = entities?.map((entity) => {
|
639
|
+
const { slug, ...rest } = "slug" in entity ? entity : { slug: `${type}Slug`, ...entity };
|
640
|
+
return [slug, rest];
|
641
|
+
});
|
642
|
+
return entries ? Object.fromEntries(entries) : void 0;
|
643
|
+
};
|
636
644
|
var transformMetaQuery = (models, dependencyStatements, statementParams, query) => {
|
637
645
|
const { queryType } = splitQuery(query);
|
638
646
|
const subAltering = query.alter && !("to" in query.alter);
|
@@ -671,14 +679,23 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
|
|
671
679
|
const newModel = jsonValue;
|
672
680
|
const modelWithFields = addDefaultModelFields(newModel, true);
|
673
681
|
const modelWithPresets = addDefaultModelPresets(models, modelWithFields);
|
674
|
-
const
|
675
|
-
|
682
|
+
const entities = Object.fromEntries(
|
683
|
+
Object.entries(PLURAL_MODEL_ENTITIES).map(([type, pluralType2]) => {
|
684
|
+
const list = modelWithPresets[pluralType2];
|
685
|
+
return [pluralType2, formatModelEntity(type, list)];
|
686
|
+
})
|
687
|
+
);
|
688
|
+
const columns = modelWithPresets.fields.map((field2) => getFieldStatement(models, modelWithPresets, field2)).filter(Boolean);
|
676
689
|
dependencyStatements.push({
|
677
690
|
statement: `${statement} (${columns.join(", ")})`,
|
678
691
|
params: []
|
679
692
|
});
|
680
693
|
models.push(modelWithPresets);
|
681
|
-
|
694
|
+
const finalModel = Object.assign({}, modelWithPresets);
|
695
|
+
for (const entity2 in entities) {
|
696
|
+
if (entities[entity2]) finalModel[entity2] = entities[entity2];
|
697
|
+
}
|
698
|
+
queryTypeDetails = { to: finalModel };
|
682
699
|
}
|
683
700
|
if (action === "alter" && model) {
|
684
701
|
const newModel = jsonValue;
|
@@ -714,10 +731,10 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
|
|
714
731
|
}
|
715
732
|
if (entity === "field" && model) {
|
716
733
|
if (action === "create") {
|
717
|
-
const
|
718
|
-
|
734
|
+
const field2 = jsonValue;
|
735
|
+
field2.type = field2.type || "string";
|
719
736
|
dependencyStatements.push({
|
720
|
-
statement: `${statement} ADD COLUMN ${getFieldStatement(models, model,
|
737
|
+
statement: `${statement} ADD COLUMN ${getFieldStatement(models, model, field2)}`,
|
721
738
|
params: []
|
722
739
|
});
|
723
740
|
} else if (action === "alter") {
|
@@ -741,15 +758,15 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
|
|
741
758
|
const params = [];
|
742
759
|
let statement2 = `${tableAction}${index?.unique ? " UNIQUE" : ""} INDEX "${indexName}"`;
|
743
760
|
if (action === "create") {
|
744
|
-
const columns = index.fields.map((
|
761
|
+
const columns = index.fields.map((field2) => {
|
745
762
|
let fieldSelector = "";
|
746
|
-
if ("slug" in
|
747
|
-
({ fieldSelector } = getFieldFromModel(model,
|
748
|
-
} else if ("expression" in
|
749
|
-
fieldSelector = parseFieldExpression(model, "to",
|
763
|
+
if ("slug" in field2) {
|
764
|
+
({ fieldSelector } = getFieldFromModel(model, field2.slug, "to"));
|
765
|
+
} else if ("expression" in field2) {
|
766
|
+
fieldSelector = parseFieldExpression(model, "to", field2.expression);
|
750
767
|
}
|
751
|
-
if (
|
752
|
-
if (
|
768
|
+
if (field2.collation) fieldSelector += ` COLLATE ${field2.collation}`;
|
769
|
+
if (field2.order) fieldSelector += ` ${field2.order}`;
|
753
770
|
return fieldSelector;
|
754
771
|
});
|
755
772
|
statement2 += ` ON "${tableName}" (${columns.join(", ")})`;
|
@@ -775,8 +792,8 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
|
|
775
792
|
fields: ["action"]
|
776
793
|
});
|
777
794
|
}
|
778
|
-
const fieldSelectors = trigger.fields.map((
|
779
|
-
return getFieldFromModel(model,
|
795
|
+
const fieldSelectors = trigger.fields.map((field2) => {
|
796
|
+
return getFieldFromModel(model, field2.slug, "to").fieldSelector;
|
780
797
|
});
|
781
798
|
statementParts.push(`OF (${fieldSelectors.join(", ")})`);
|
782
799
|
}
|
@@ -808,10 +825,23 @@ var transformMetaQuery = (models, dependencyStatements, statementParams, query)
|
|
808
825
|
dependencyStatements.push({ statement: statement2, params });
|
809
826
|
}
|
810
827
|
const pluralType = PLURAL_MODEL_ENTITIES[entity];
|
811
|
-
const
|
812
|
-
let json
|
813
|
-
|
814
|
-
|
828
|
+
const field = `${RONIN_MODEL_SYMBOLS.FIELD}${pluralType}`;
|
829
|
+
let json;
|
830
|
+
switch (action) {
|
831
|
+
case "create": {
|
832
|
+
const value = prepareStatementValue(statementParams, jsonValue);
|
833
|
+
json = `json_insert(${field}, '$.${slug}', ${value})`;
|
834
|
+
break;
|
835
|
+
}
|
836
|
+
case "alter": {
|
837
|
+
const value = prepareStatementValue(statementParams, jsonValue);
|
838
|
+
json = `json_set(${field}, '$.${slug}', json_patch(json_extract(${field}, '$.${slug}'), ${value}))`;
|
839
|
+
break;
|
840
|
+
}
|
841
|
+
case "drop": {
|
842
|
+
json = `json_remove(${field}, '$.${slug}')`;
|
843
|
+
}
|
844
|
+
}
|
815
845
|
return {
|
816
846
|
set: {
|
817
847
|
model: {
|
@@ -1321,33 +1351,64 @@ var compileQueryInput = (query, models, statementParams, options) => {
|
|
1321
1351
|
};
|
1322
1352
|
|
1323
1353
|
// src/index.ts
|
1324
|
-
var
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1354
|
+
var Transaction = class {
|
1355
|
+
statements;
|
1356
|
+
models;
|
1357
|
+
queries;
|
1358
|
+
constructor(queries, options) {
|
1359
|
+
const models = options?.models || [];
|
1360
|
+
this.statements = this.compileQueries(queries, models, options);
|
1361
|
+
this.models = models;
|
1362
|
+
this.queries = queries;
|
1363
|
+
}
|
1364
|
+
/**
|
1365
|
+
* Composes SQL statements for the provided RONIN queries.
|
1366
|
+
*
|
1367
|
+
* @param queries - The RONIN queries for which SQL statements should be composed.
|
1368
|
+
* @param models - A list of models.
|
1369
|
+
* @param options - Additional options to adjust the behavior of the statement generation.
|
1370
|
+
*
|
1371
|
+
* @returns The composed SQL statements.
|
1372
|
+
*/
|
1373
|
+
compileQueries = (queries, models, options) => {
|
1374
|
+
const modelList = addSystemModels(models).map((model) => {
|
1375
|
+
return addDefaultModelFields(model, true);
|
1376
|
+
});
|
1377
|
+
const modelListWithPresets = modelList.map((model) => {
|
1378
|
+
return addDefaultModelPresets(modelList, model);
|
1379
|
+
});
|
1380
|
+
const dependencyStatements = [];
|
1381
|
+
const mainStatements = [];
|
1382
|
+
for (const query of queries) {
|
1383
|
+
const statementValues = options?.inlineParams ? null : [];
|
1384
|
+
const transformedQuery = transformMetaQuery(
|
1385
|
+
modelListWithPresets,
|
1386
|
+
dependencyStatements,
|
1387
|
+
statementValues,
|
1388
|
+
query
|
1389
|
+
);
|
1390
|
+
const result = compileQueryInput(
|
1391
|
+
transformedQuery,
|
1392
|
+
modelListWithPresets,
|
1393
|
+
statementValues
|
1394
|
+
);
|
1395
|
+
dependencyStatements.push(...result.dependencies);
|
1396
|
+
mainStatements.push(result.main);
|
1397
|
+
}
|
1398
|
+
return [...dependencyStatements, ...mainStatements];
|
1399
|
+
};
|
1400
|
+
formatOutput(results) {
|
1401
|
+
return results.map((result, index) => {
|
1402
|
+
const query = this.queries.at(-index);
|
1403
|
+
const { queryType, queryModel } = splitQuery(query);
|
1404
|
+
const model = getModelBySlug(this.models, queryModel);
|
1405
|
+
const single = queryModel !== model.pluralSlug;
|
1406
|
+
if (single) return { record: result[0] };
|
1407
|
+
if (queryType === "count") return { amount: result[0] };
|
1408
|
+
return { records: result };
|
1409
|
+
});
|
1348
1410
|
}
|
1349
|
-
return [...dependencyStatements, ...mainStatements];
|
1350
1411
|
};
|
1351
1412
|
export {
|
1352
|
-
|
1413
|
+
Transaction
|
1353
1414
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ronin/compiler",
|
3
|
-
"version": "0.8.
|
3
|
+
"version": "0.8.8-leo-ron-1083-experimental-191",
|
4
4
|
"type": "module",
|
5
5
|
"description": "Compiles RONIN queries to SQL statements.",
|
6
6
|
"publishConfig": {
|
@@ -33,6 +33,7 @@
|
|
33
33
|
},
|
34
34
|
"devDependencies": {
|
35
35
|
"@biomejs/biome": "1.9.2",
|
36
|
+
"@ronin/engine": "0.0.2",
|
36
37
|
"@types/bun": "1.1.10",
|
37
38
|
"@types/title": "3.4.3",
|
38
39
|
"tsup": "8.3.0",
|