Skip to content

Commit

Permalink
Add autocomplete for properties
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel committed Sep 27, 2024
1 parent 9500a2e commit 7af3af0
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 51 deletions.
2 changes: 1 addition & 1 deletion libs/aml/resources/full.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"schema": "cms",
"name": "posts",
"attrs": [
{"name": "id", "type": "int", "extra": {"autoIncrement": true, "tags": ["id"]}},
{"name": "id", "type": "int", "extra": {"autoIncrement": null, "tags": ["id"]}},
{"name": "title", "type": "varchar(100)"},
{"name": "status", "type": "post_status"},
{"name": "content", "type": "varchar", "null": true},
Expand Down
2 changes: 1 addition & 1 deletion libs/aml/src/amlBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function buildTypeContent(namespace: Namespace, statement: number, t: TypeConten
function buildExtra(extra: Extra, v: {properties?: PropertiesAst}, ignore: string[]): Extra {
const properties = v?.properties
?.filter(p => !ignore.includes(p.key.value))
?.reduce((acc, prop) => ({...acc, [prop.key.value]: prop.value ? buildPropValue(prop.value) : true}), {} as Record<string, PropertyValue | undefined>) || {}
?.reduce((acc, prop) => ({...acc, [prop.key.value]: prop.value ? buildPropValue(prop.value) : null}), {} as Record<string, PropertyValue | undefined>) || {}
return {...properties, ...removeEmpty(extra)}
}

Expand Down
16 changes: 9 additions & 7 deletions libs/aml/src/amlGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ type range \`(subtype = float8, subtype_diff = float8mi)\` # custom type
indexes: [{attrs: [['title']]}],
checks: [{attrs: [['author']], predicate: 'author > 0', name: 'has_author_chk'}],
doc: 'all posts',
extra: {line: 29, statement: 3, comment: 'an other entity', pii: true, tags: ['cms']}
extra: {line: 29, statement: 3, comment: 'an other entity', pii: null, tags: ['cms']}
}, {
name: 'emails',
attrs: [
Expand All @@ -118,14 +118,14 @@ type range \`(subtype = float8, subtype_diff = float8mi)\` # custom type
}],
relations: [
{src: {schema: 'identity', entity: 'users'}, ref: {entity: 'countries'}, attrs: [{src: ['settings', 'address', 'country'], ref: ['id']}], extra: {line: 26, statement: 2, natural: 'ref', inline: true}},
{src: {entity: 'posts'}, ref: {entity: 'users'}, attrs: [{src: ['author'], ref: ['id']}], extra: {line: 32, statement: 3, inline: true}},
{src: {entity: 'posts'}, ref: {entity: 'users'}, attrs: [{src: ['created_by'], ref: ['id']}], doc: 'standalone relation', extra: {line: 35, statement: 4, onUpdate: 'no action', onDelete: 'cascade'}},
{src: {entity: 'posts'}, ref: {schema: 'identity', entity: 'users'}, attrs: [{src: ['author'], ref: ['id']}], extra: {line: 32, statement: 3, inline: true, refAlias: 'users'}},
{src: {entity: 'posts'}, ref: {schema: 'identity', entity: 'users'}, attrs: [{src: ['created_by'], ref: ['id']}], doc: 'standalone relation', extra: {line: 35, statement: 4, refAlias: 'users', onUpdate: 'no action', onDelete: 'cascade'}},
],
types: [
{schema: 'identity', name: 'user_role', values: ['admin', 'guest'], extra: {line: 15, statement: 2, inline: true}},
{name: 'post_id', alias: 'int', doc: 'alias', extra: {line: 43, statement: 6, table: 'posts'}},
{name: 'status', values: ['draft', 'published', 'archived'], extra: {line: 44, statement: 7}},
{name: 'position', attrs: [{name: 'x', type: 'int'}, {name: 'y', type: 'int'}], extra: {line: 45, statement: 8, generic: true}},
{name: 'position', attrs: [{name: 'x', type: 'int'}, {name: 'y', type: 'int'}], extra: {line: 45, statement: 8, generic: null}},
{name: 'range', definition: '(subtype = float8, subtype_diff = float8mi)', extra: {line: 46, statement: 9, comment: 'custom type'}},
],
extra: {comments: [
Expand All @@ -140,7 +140,9 @@ type range \`(subtype = float8, subtype_diff = float8mi)\` # custom type
expect(generateAml(parsed.result || {})).toEqual(input)
})
test('full', () => {
const db: Database = parseJsonDatabase(fs.readFileSync('./resources/full.json', 'utf8')).result || {}
const json = parseJsonDatabase(fs.readFileSync('./resources/full.json', 'utf8'))
expect(json.errors).toEqual(undefined)
const db: Database = json.result || {}
const aml = fs.readFileSync('./resources/full.aml', 'utf8')
const parsed = parseAmlTest(aml)
expect(parsed).toEqual({result: db})
Expand Down Expand Up @@ -463,8 +465,8 @@ fk admins.id -> users.id
name: "users",
attrs: [
{name: 'id', type: 'int'},
{name: 'role', type: 'varchar', default: 'guest', extra: {hidden: true}},
{name: 'score', type: 'double precision', default: 0, doc: 'User progression', extra: {comment: 'a column with almost all possible attributes', hidden: true}},
{name: 'role', type: 'varchar', default: 'guest', extra: {hidden: null}},
{name: 'score', type: 'double precision', default: 0, doc: 'User progression', extra: {comment: 'a column with almost all possible attributes', hidden: null}},
{name: 'first_name', type: 'varchar(10)'},
{name: 'last_name', type: 'varchar(10)'},
{name: 'email', type: 'varchar', null: true},
Expand Down
4 changes: 2 additions & 2 deletions libs/aml/src/amlGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ function genRelation(r: Relation, namespace: Namespace, legacy: boolean): string
if (legacy && r.attrs.length > 1) return r.attrs.map(attr => genRelation({...r, attrs: [attr]}, namespace, legacy)).join('') // in v1 composite relations are defined as several relations
if (legacy && (r.extra?.natural === 'both' || r.extra?.natural === 'src')) return '' // v1 doesn't support src natural relation
const srcNatural: boolean = !r.extra?.inline && (r.extra?.natural === 'src' || r.extra?.natural === 'both')
const props = !legacy ? genProperties(r.extra, {}, ['line', 'statement', 'inline', 'natural', 'comment']) : ''
const props = !legacy ? genProperties(r.extra, {}, ['line', 'statement', 'inline', 'natural', 'srcAlias', 'refAlias', 'comment']) : ''
return `${legacy ? 'fk' : 'rel'} ${genAttributeRef(r.src, r.attrs.map(a => a.src), namespace, srcNatural, r.extra?.srcAlias, legacy)} ${genRelationTarget(r, namespace, true, legacy)}${props}${genDoc(r.doc, legacy)}${genComment(r.extra?.comment)}\n`
}

Expand Down Expand Up @@ -183,7 +183,7 @@ function genName(e: Namespace & { name: string }, n: Namespace, legacy: boolean)

function genProperties(extra: Extra | undefined, additional: Extra, ignore: string[]): string {
const entries = Object.entries(additional).concat(Object.entries(extra || {})).filter(([k, ]) => !ignore.includes(k))
return entries.length > 0 ? ' {' + entries.map(([key, value]) => value !== undefined && value !== true ? `${key}: ${genPropertyValue(value)}` : key).join(', ') + '}' : ''
return entries.length > 0 ? ' {' + entries.map(([key, value]) => value !== undefined && value !== null ? `${key}: ${genPropertyValue(value)}` : key).join(', ') + '}' : ''
}

function genPropertyValue(v: PropertyValue): string {
Expand Down
3 changes: 2 additions & 1 deletion libs/aml/src/amlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ class AmlParser extends EmbeddedActionsParser {
SEP: Comma,
DEF: () => {
$.OPTION(() => $.CONSUME(WhiteSpace))
values.push($.SUBRULE(propertyValueRule))
const value = $.SUBRULE(propertyValueRule)
if (value) values.push(value) // on invalid input, `value` can be undefined :/
$.OPTION2(() => $.CONSUME2(WhiteSpace))
}
})
Expand Down
49 changes: 46 additions & 3 deletions libs/aml/src/extensions/monaco.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import {Entity} from "@azimutt/models";
import {
attributeIndentationMatch,
attributeNameWrittenMatch,
attributeTypeWrittenMatch,
attributeNestedMatch,
attributePropsKeyMatch,
attributePropsValueMatch,
attributeRootMatch,
attributeTypeWrittenMatch,
entityPropsKeyMatch,
entityPropsValueMatch,
entityWrittenMatch,
relationLinkWrittenMatch,
relationPropsKeyMatch,
relationPropsValueMatch,
relationSrcWrittenMatch,
suggestAttributeType,
suggestExtra,
Expand Down Expand Up @@ -87,17 +93,54 @@ describe('Monaco AML', () => {
expect(relationSrcWrittenMatch('rel users(id, name) ')).toEqual(['users(id, name)'])
expect(relationSrcWrittenMatch('rel web.public.users(id, settings.addess.street) ')).toEqual(['web.public.users(id, settings.addess.street)'])
})
test('(entity|attribute|relation)PropsKeyMatch', () => {
[{prefix: 'users', match: entityPropsKeyMatch}, {prefix: ' id', match: attributePropsKeyMatch}, {prefix: 'rel posts(author) -> users(id)', match: relationPropsKeyMatch}].forEach(matcher => {
expect(matcher.match(``)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix}`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {`)).toEqual([])
expect(matcher.match(`${matcher.prefix} {pii`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii,`)).toEqual(['pii'])
expect(matcher.match(`${matcher.prefix} {pii, `)).toEqual(['pii'])
expect(matcher.match(`${matcher.prefix} {pii, color`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii, color:`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii, color: `)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii, color: red`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii, color: red,`)).toEqual(['pii', 'color'])
expect(matcher.match(`${matcher.prefix} {pii, color: red, `)).toEqual(['pii', 'color'])
expect(matcher.match(`${matcher.prefix} {pii, color: red, t`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {tags: [pii], `)).toEqual(['tags'])
// expect(matcher.match(`${matcher.prefix} {tags: [pii, deprecated], `)).toEqual(['tags']) // FIXME: fails on nested ','
expect(matcher.match(`${matcher.prefix} {view: "SELECT * FROM users", `)).toEqual(['view'])
})
})
test('(entity|attribute|relation)PropsValueMatch', () => {
[{prefix: 'users', match: entityPropsValueMatch}, {prefix: ' id', match: attributePropsValueMatch}, {prefix: 'rel posts(author) -> users(id)', match: relationPropsValueMatch}].forEach(matcher => {
expect(matcher.match(`${matcher.prefix}`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii:`)).toEqual(['pii'])
expect(matcher.match(`${matcher.prefix} {pii: `)).toEqual(['pii'])
expect(matcher.match(`${matcher.prefix} {pii: true`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii: true, color`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii: true, color:`)).toEqual(['color'])
expect(matcher.match(`${matcher.prefix} {pii: true, color: `)).toEqual(['color'])
expect(matcher.match(`${matcher.prefix} {pii: true, color: r`)).toEqual(undefined)
expect(matcher.match(`${matcher.prefix} {pii: true, tags: [`)).toEqual(['tags'])
// expect(matcher.match(`${matcher.prefix} {pii: true, tags: [pii,`)).toEqual(['tags']) // FIXME: fails on nested ','
expect(matcher.match(`${matcher.prefix} {pii: true, view: "`)).toEqual(['view'])
})
})
})
describe('suggestions', () => {
const pos: Position = {column: 0, lineNumber: 0}
test('suggestAttributeType', () => {
const basic: CompletionItem[] = []
suggestAttributeType(basic, pos, [])
suggestAttributeType([], [], basic, pos)
expect(basic.map(e => e.insertText).includes('varchar')).toBeTruthy()
expect(basic.map(e => e.insertText).includes('public.box')).toBeFalsy()

const custom: CompletionItem[] = []
suggestAttributeType(custom, pos, [{schema: 'public', name: 'box'}])
suggestAttributeType([{schema: 'public', name: 'box'}], [], custom, pos)
expect(custom.map(e => e.insertText).includes('varchar')).toBeTruthy()
expect(custom.map(e => e.insertText).includes('public.box')).toBeTruthy()
})
Expand Down
Loading

0 comments on commit 7af3af0

Please sign in to comment.