@ronin/compiler
Advanced tools
Comparing version 0.1.0 to 0.1.1-leo-ron-1083-experimental.6
@@ -85,2 +85,3 @@ // src/utils/index.ts | ||
// src/utils/schema.ts | ||
import title from "title"; | ||
var getSchemaBySlug = (schemas, slug) => { | ||
@@ -101,5 +102,2 @@ const schema = schemas.find((schema2) => { | ||
}; | ||
var getSchemaName = (schema) => { | ||
return schema.name || schema.slug; | ||
}; | ||
var composeMetaSchemaSlug = (suffix) => convertToCamelCase(`ronin_${suffix}`); | ||
@@ -131,3 +129,3 @@ var composeAssociationSchemaSlug = (schema, field) => composeMetaSchemaSlug(`${schema.pluralSlug}_${field.slug}`); | ||
throw new RoninError({ | ||
message: `${errorPrefix} does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `${errorPrefix} does not exist in schema "${schema.name}".`, | ||
code: "FIELD_NOT_FOUND", | ||
@@ -165,4 +163,3 @@ field: fieldPath, | ||
name: "RONIN - Created By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.createdBy" | ||
@@ -177,4 +174,3 @@ }, | ||
name: "RONIN - Updated By", | ||
type: "reference", | ||
schema: "account", | ||
type: "string", | ||
slug: "ronin.updatedBy" | ||
@@ -209,14 +205,54 @@ } | ||
{ slug: "name", type: "string" }, | ||
{ slug: "slug", type: "string" }, | ||
{ slug: "type", type: "string" }, | ||
{ slug: "schema", type: "reference", schema: "schema" }, | ||
{ slug: "slug", type: "string", required: true }, | ||
{ slug: "type", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "required", type: "boolean" }, | ||
{ slug: "defaultValue", type: "string" }, | ||
{ slug: "unique", type: "boolean" }, | ||
{ slug: "autoIncrement", type: "boolean" } | ||
{ slug: "autoIncrement", type: "boolean" }, | ||
// Only allowed for fields of type "reference". | ||
{ slug: "target", type: "reference", target: { slug: "schema" } }, | ||
{ slug: "kind", type: "string" }, | ||
{ slug: "actions", type: "group" }, | ||
{ slug: "actions.onDelete", type: "string" }, | ||
{ slug: "actions.onUpdate", type: "string" } | ||
] | ||
}, | ||
{ | ||
name: "Index", | ||
pluralName: "Indexes", | ||
slug: "index", | ||
pluralSlug: "indexes", | ||
fields: [ | ||
...SYSTEM_FIELDS, | ||
{ slug: "slug", type: "string", required: true }, | ||
{ | ||
slug: "schema", | ||
type: "reference", | ||
target: { slug: "schema" }, | ||
required: true | ||
}, | ||
{ slug: "unique", type: "boolean" }, | ||
{ slug: "filter", type: "json" } | ||
] | ||
} | ||
]; | ||
var SYSTEM_SCHEMA_SLUGS = SYSTEM_SCHEMAS.flatMap(({ slug, pluralSlug }) => [ | ||
slug, | ||
pluralSlug | ||
]); | ||
var addSystemSchemas = (schemas) => { | ||
const list = [...SYSTEM_SCHEMAS, ...schemas].map((schema) => ({ ...schema })); | ||
const list = [...SYSTEM_SCHEMAS, ...schemas].map((schema) => { | ||
const copiedSchema = { ...schema }; | ||
if (!copiedSchema.pluralSlug) copiedSchema.pluralSlug = pluralize(copiedSchema.slug); | ||
if (!copiedSchema.name) copiedSchema.name = slugToName(copiedSchema.slug); | ||
if (!copiedSchema.pluralName) | ||
copiedSchema.pluralName = slugToName(copiedSchema.pluralSlug); | ||
return copiedSchema; | ||
}); | ||
for (const schema of list) { | ||
@@ -226,3 +262,3 @@ const defaultIncluding = {}; | ||
if (field.type === "reference" && !field.slug.startsWith("ronin.")) { | ||
const relatedSchema = getSchemaBySlug(list, field.schema); | ||
const relatedSchema = getSchemaBySlug(list, field.target.slug); | ||
let fieldSlug = relatedSchema.slug; | ||
@@ -236,5 +272,5 @@ if (field.kind === "many") { | ||
{ | ||
slug: "origin", | ||
slug: "source", | ||
type: "reference", | ||
schema: schema.slug | ||
target: schema | ||
}, | ||
@@ -244,3 +280,3 @@ { | ||
type: "reference", | ||
schema: relatedSchema.slug | ||
target: relatedSchema | ||
} | ||
@@ -261,3 +297,3 @@ ] | ||
}; | ||
const relatedSchemaToModify = list.find((schema2) => schema2.slug === field.schema); | ||
const relatedSchemaToModify = getSchemaBySlug(list, field.target.slug); | ||
if (!relatedSchemaToModify) throw new Error("Missing related schema"); | ||
@@ -305,2 +341,12 @@ relatedSchemaToModify.including = { | ||
statement += ` DEFAULT ${field.defaultValue}`; | ||
if (field.type === "reference") { | ||
const actions = field.actions || {}; | ||
const targetTable = convertToSnakeCase(field.target.slug); | ||
statement += ` REFERENCES ${targetTable}("id")`; | ||
for (const trigger in actions) { | ||
const triggerName = trigger.toUpperCase().slice(2); | ||
const action = actions[trigger]; | ||
statement += ` ON ${triggerName} ${action}`; | ||
} | ||
} | ||
return statement; | ||
@@ -311,14 +357,11 @@ }; | ||
if (!["create", "set", "drop"].includes(queryType)) return; | ||
if (!["schema", "schemas", "field", "fields"].includes(querySchema)) return; | ||
if (!SYSTEM_SCHEMA_SLUGS.includes(querySchema)) return; | ||
const instructionName = mappedInstructions[queryType]; | ||
const instructionList = queryInstructions[instructionName]; | ||
const kind = ["schema", "schemas"].includes(querySchema) ? "schemas" : "fields"; | ||
const instructionTarget = kind === "schemas" ? instructionList : instructionList?.schema; | ||
const kind = getSchemaBySlug(SYSTEM_SCHEMAS, querySchema).pluralSlug; | ||
let tableAction = "ALTER"; | ||
let schemaPluralSlug = null; | ||
let queryTypeReadable = null; | ||
switch (queryType) { | ||
case "create": { | ||
if (kind === "schemas") tableAction = "CREATE"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug; | ||
if (kind === "schemas" || kind === "indexes") tableAction = "CREATE"; | ||
queryTypeReadable = "creating"; | ||
@@ -329,3 +372,2 @@ break; | ||
if (kind === "schemas") tableAction = "ALTER"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
queryTypeReadable = "updating"; | ||
@@ -335,4 +377,3 @@ break; | ||
case "drop": { | ||
if (kind === "schemas") tableAction = "DROP"; | ||
schemaPluralSlug = instructionTarget?.pluralSlug?.being || instructionTarget?.pluralSlug; | ||
if (kind === "schemas" || kind === "indexes") tableAction = "DROP"; | ||
queryTypeReadable = "deleting"; | ||
@@ -342,14 +383,31 @@ break; | ||
} | ||
if (!schemaPluralSlug) { | ||
const field = kind === "schemas" ? "pluralSlug" : "schema.pluralSlug"; | ||
const slug = instructionList?.slug?.being || instructionList?.slug; | ||
if (!slug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} ${kind}, a \`${field}\` field must be provided in the \`${instructionName}\` instruction.`, | ||
message: `When ${queryTypeReadable} ${kind}, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: [field] | ||
fields: ["slug"] | ||
}); | ||
} | ||
const table = convertToSnakeCase(schemaPluralSlug); | ||
const fields = [...SYSTEM_FIELDS]; | ||
let statement = `${tableAction} TABLE "${table}"`; | ||
const schemaInstruction = instructionList?.schema; | ||
const schemaSlug = schemaInstruction?.slug?.being || schemaInstruction?.slug; | ||
if (kind !== "schemas" && !schemaSlug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} ${kind}, a \`schema.slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: ["schema.slug"] | ||
}); | ||
} | ||
const tableName = convertToSnakeCase(kind === "schemas" ? slug : schemaSlug); | ||
if (kind === "indexes") { | ||
const indexName = convertToSnakeCase(slug); | ||
const unique = instructionList?.unique; | ||
let statement2 = `${tableAction}${unique ? " UNIQUE" : ""} INDEX "${indexName}"`; | ||
if (queryType === "create") statement2 += ` ON "${tableName}"`; | ||
writeStatements.push(statement2); | ||
return; | ||
} | ||
let statement = `${tableAction} TABLE "${tableName}"`; | ||
if (kind === "schemas") { | ||
const fields = [...SYSTEM_FIELDS]; | ||
if (queryType === "create") { | ||
@@ -359,3 +417,3 @@ const columns = fields.map(getFieldStatement).filter(Boolean); | ||
} else if (queryType === "set") { | ||
const newSlug = queryInstructions.to?.pluralSlug; | ||
const newSlug = queryInstructions.to?.slug; | ||
if (newSlug) { | ||
@@ -366,11 +424,6 @@ const newTable = convertToSnakeCase(newSlug); | ||
} | ||
} else if (kind === "fields") { | ||
const fieldSlug = instructionTarget?.slug?.being || instructionList?.slug; | ||
if (!fieldSlug) { | ||
throw new RoninError({ | ||
message: `When ${queryTypeReadable} fields, a \`slug\` field must be provided in the \`${instructionName}\` instruction.`, | ||
code: "MISSING_FIELD", | ||
fields: ["slug"] | ||
}); | ||
} | ||
writeStatements.push(statement); | ||
return; | ||
} | ||
if (kind === "fields") { | ||
if (queryType === "create") { | ||
@@ -388,26 +441,50 @@ if (!instructionList.type) { | ||
if (newSlug) { | ||
statement += ` RENAME COLUMN "${fieldSlug}" TO "${newSlug}"`; | ||
statement += ` RENAME COLUMN "${slug}" TO "${newSlug}"`; | ||
} | ||
} else if (queryType === "drop") { | ||
statement += ` DROP COLUMN "${fieldSlug}"`; | ||
statement += ` DROP COLUMN "${slug}"`; | ||
} | ||
writeStatements.push(statement); | ||
} | ||
writeStatements.push(statement); | ||
}; | ||
var slugToName = (slug) => { | ||
const name = slug.replace(/([a-z])([A-Z])/g, "$1 $2"); | ||
return title(name); | ||
}; | ||
var VOWELS = ["a", "e", "i", "o", "u"]; | ||
var pluralize = (word) => { | ||
const lastLetter = word.slice(-1).toLowerCase(); | ||
const secondLastLetter = word.slice(-2, -1).toLowerCase(); | ||
if (lastLetter === "y" && !VOWELS.includes(secondLastLetter)) { | ||
return `${word.slice(0, -1)}ies`; | ||
} | ||
if (lastLetter === "s" || word.slice(-2).toLowerCase() === "ch" || word.slice(-2).toLowerCase() === "sh" || word.slice(-2).toLowerCase() === "ex") { | ||
return `${word}es`; | ||
} | ||
return `${word}s`; | ||
}; | ||
// src/instructions/with.ts | ||
var WITH_CONDITIONS = [ | ||
"being", | ||
"notBeing", | ||
"startingWith", | ||
"notStartingWith", | ||
"endingWith", | ||
"notEndingWith", | ||
"containing", | ||
"notContaining", | ||
"greaterThan", | ||
"greaterOrEqual", | ||
"lessThan", | ||
"lessOrEqual" | ||
]; | ||
var getMatcher = (value, negative) => { | ||
if (negative) { | ||
if (value === null) return "IS NOT"; | ||
return "!="; | ||
} | ||
if (value === null) return "IS"; | ||
return "="; | ||
}; | ||
var WITH_CONDITIONS = { | ||
being: (value, baseValue) => `${getMatcher(baseValue, false)} ${value}`, | ||
notBeing: (value, baseValue) => `${getMatcher(baseValue, true)} ${value}`, | ||
startingWith: (value) => `LIKE ${value}%`, | ||
notStartingWith: (value) => `NOT LIKE ${value}%`, | ||
endingWith: (value) => `LIKE %${value}`, | ||
notEndingWith: (value) => `NOT LIKE %${value}`, | ||
containing: (value) => `LIKE %${value}%`, | ||
notContaining: (value) => `NOT LIKE %${value}%`, | ||
greaterThan: (value) => `> ${value}`, | ||
greaterOrEqual: (value) => `>= ${value}`, | ||
lessThan: (value) => `< ${value}`, | ||
lessOrEqual: (value) => `<= ${value}` | ||
}; | ||
var handleWith = (schemas, schema, statementValues, instruction, rootTable) => { | ||
@@ -468,23 +545,7 @@ const subStatement = composeConditions( | ||
if (options.type === "values") return conditionValue; | ||
const conditionTypes = { | ||
being: [getMatcher(value, false), conditionValue], | ||
notBeing: [getMatcher(value, true), conditionValue], | ||
startingWith: ["LIKE", `${conditionValue}%`], | ||
notStartingWith: ["NOT LIKE", `${conditionValue}%`], | ||
endingWith: ["LIKE", `%${conditionValue}`], | ||
notEndingWith: ["NOT LIKE", `%${conditionValue}`], | ||
containing: ["LIKE", `%${conditionValue}%`], | ||
notContaining: ["NOT LIKE", `%${conditionValue}%`], | ||
greaterThan: [">", conditionValue], | ||
greaterOrEqual: [">=", conditionValue], | ||
lessThan: ["<", conditionValue], | ||
lessOrEqual: ["<=", conditionValue] | ||
}; | ||
return `${conditionSelector} ${conditionTypes[options.condition || "being"].join(" ")}`; | ||
return `${conditionSelector} ${WITH_CONDITIONS[options.condition || "being"](conditionValue, value)}`; | ||
}; | ||
var composeConditions = (schemas, schema, statementValues, instructionName, value, options) => { | ||
const isNested = isObject(value) && Object.keys(value).length > 0; | ||
if (isNested && Object.keys(value).every( | ||
(key) => WITH_CONDITIONS.includes(key) | ||
)) { | ||
if (isNested && Object.keys(value).every((key) => key in WITH_CONDITIONS)) { | ||
const conditions = Object.entries(value).map( | ||
@@ -525,3 +586,3 @@ ([conditionType, checkValue]) => composeConditions(schemas, schema, statementValues, instructionName, checkValue, { | ||
} else { | ||
const relatedSchema = getSchemaBySlug(schemas, schemaField.schema); | ||
const relatedSchema = getSchemaBySlug(schemas, schemaField.target.slug); | ||
const subQuery = { | ||
@@ -580,10 +641,2 @@ get: { | ||
}; | ||
var getMatcher = (value, negative) => { | ||
if (negative) { | ||
if (value === null) return "IS NOT"; | ||
return "!="; | ||
} | ||
if (value === null) return "IS"; | ||
return "="; | ||
}; | ||
var formatIdentifiers = ({ identifiers }, queryInstructions) => { | ||
@@ -699,3 +752,3 @@ if (!queryInstructions) return queryInstructions; | ||
throw new RoninError({ | ||
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `The provided \`for\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`, | ||
code: "INVALID_FOR_VALUE" | ||
@@ -732,3 +785,3 @@ }); | ||
throw new RoninError({ | ||
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${getSchemaName(schema)}".`, | ||
message: `The provided \`including\` shortcut "${shortcut}" does not exist in schema "${schema.name}".`, | ||
code: "INVALID_INCLUDING_VALUE" | ||
@@ -901,4 +954,4 @@ }); | ||
const composeStatement = (subQueryType, value) => { | ||
const origin = queryType === "create" ? { id: toInstruction.id } : withInstruction; | ||
const recordDetails = { origin }; | ||
const source = queryType === "create" ? { id: toInstruction.id } : withInstruction; | ||
const recordDetails = { source }; | ||
if (value) recordDetails.target = value; | ||
@@ -905,0 +958,0 @@ const { readStatement } = compileQueryInput( |
{ | ||
"name": "@ronin/compiler", | ||
"version": "0.1.0", | ||
"version": "0.1.1-leo-ron-1083-experimental.6", | ||
"type": "module", | ||
@@ -11,3 +11,5 @@ "description": "Compiles RONIN queries to SQL statements.", | ||
"types": "./dist/index.d.ts", | ||
"files": ["dist"], | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
@@ -22,7 +24,12 @@ "lint": "bun run --bun lint:tsc && bun run --bun lint:biome", | ||
}, | ||
"keywords": ["query", "compiler", "sql"], | ||
"keywords": [ | ||
"query", | ||
"compiler", | ||
"sql" | ||
], | ||
"author": "ronin", | ||
"license": "MIT", | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"@paralleldrive/cuid2": "2.2.2" | ||
"@paralleldrive/cuid2": "2.2.2", | ||
"title": "3.5.3" | ||
}, | ||
@@ -32,2 +39,3 @@ "devDependencies": { | ||
"@types/bun": "1.1.10", | ||
"@types/title": "3.4.3", | ||
"tsup": "8.3.0", | ||
@@ -34,0 +42,0 @@ "typescript": "5.6.2", |
# RONIN Compiler | ||
This package compiles RONIN queries to SQL statements. | ||
This package compiles [RONIN queries](https://ronin.co/docs/queries) to SQL statements. | ||
## Usage | ||
## Setup | ||
You don't need to install this package explicitly, as it is already included in the [RONIN client](https://github.com/ronin-co/client). | ||
However, we would be excited to welcome your feature suggestions or bug fixes for the RONIN compiler. Read on to learn more about how to suggest changes. | ||
## Contributing | ||
To start contributing code, first make sure you have [Bun](https://bun.sh) installed, which is a JavaScript runtime. | ||
Next, [clone the repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and install its dependencies: | ||
```bash | ||
bun install | ||
``` | ||
Once that's done, link the package to make it available to all of your local projects: | ||
```bash | ||
bun link | ||
``` | ||
Inside your project, you can then run the following command, which is similar to `bun add @ronin/compiler` or `npm install @ronin/compiler`, except that it doesn't install `@ronin/compiler` from npm, but instead uses your local clone of the package: | ||
```bash | ||
bun link @ronin/compiler | ||
``` | ||
If your project is not yet compatible with [Bun](https://bun.sh), feel free to replace all of the occurrences of the word `bun` in the commands above with `npm` instead. | ||
You will just need to make sure that, once you [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request#creating-the-pull-request) on the current repo, it will not contain a `package-lock.json` file, which is usually generated by npm. Instead, we're using the `bun.lockb` file for this purpose (locking sub dependencies to a certain version). | ||
### Developing | ||
The programmatic API of the RONIN compiler looks like this: | ||
```typescript | ||
@@ -18,3 +52,2 @@ import { compileQueryInput } from '@ronin/compiler'; | ||
{ | ||
pluralSlug: 'accounts', | ||
slug: 'account', | ||
@@ -30,8 +63,22 @@ }, | ||
## Testing | ||
In order to be compatible with a wide range of projects, the source code of the `compiler` repo needs to be compiled (transpiled) whenever you make changes to it. To automate this, you can keep this command running in your terminal: | ||
Use the following command to run the test suite: | ||
```bash | ||
bun run dev | ||
``` | ||
``` | ||
Whenever you make a change to the source code, it will then automatically be transpiled again. | ||
### Running Tests | ||
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. | ||
Before you create a pull request on the `compiler` repo, it is therefore advised to run those tests in order to ensure everything works as expected: | ||
```bash | ||
# Run all tests | ||
bun test | ||
# Alternatively, run a single test | ||
bun test -t 'your test name' | ||
``` |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unpopular package
QualityThis package is not very popular.
Found 1 instance in 1 package
362271
5
5846
0
83
2
6
1
+ Addedtitle@3.5.3
+ Addedansi-styles@3.2.1(transitive)
+ Addedarch@2.2.0(transitive)
+ Addedarg@1.0.0(transitive)
+ Addedchalk@2.3.0(transitive)
+ Addedclipboardy@1.2.2(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addedcross-spawn@5.1.0(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedexeca@0.8.0(transitive)
+ Addedget-stream@3.0.0(transitive)
+ Addedhas-flag@2.0.0(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedlru-cache@4.1.5(transitive)
+ Addednpm-run-path@2.0.2(transitive)
+ Addedp-finally@1.0.0(transitive)
+ Addedpath-key@2.0.1(transitive)
+ Addedpseudomap@1.0.2(transitive)
+ Addedshebang-command@1.2.0(transitive)
+ Addedshebang-regex@1.0.0(transitive)
+ Addedsignal-exit@3.0.7(transitive)
+ Addedstrip-eof@1.0.0(transitive)
+ Addedsupports-color@4.5.0(transitive)
+ Addedtitle@3.5.3(transitive)
+ Addedtitleize@1.0.0(transitive)
+ Addedwhich@1.3.1(transitive)
+ Addedyallist@2.1.2(transitive)