Skip to content

Commit

Permalink
feat: improve component reusability
Browse files Browse the repository at this point in the history
  • Loading branch information
shah committed Jan 11, 2024
1 parent ec9c98a commit a3e9318
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 191 deletions.
91 changes: 75 additions & 16 deletions pattern/sqlpage/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export interface Component<
Name extends string = "shell" | "list" | "text" | "table" | "breadcrumb",
> extends SQLa.SqlTextSupplier<EmitContext> {
readonly name: Name;
readonly condition?:
| { anyExists: string | string[] }
| { allExist: string | string[] }
| { where: SQLa.SqlTextSupplier<EmitContext> };
}

export type FlexibleText<EmitContext extends SQLa.SqlEmitContext> =
Expand Down Expand Up @@ -104,14 +108,34 @@ export class ComponentBuilder<

component(
name: Name,
conditional?: Component<EmitContext, Name>["condition"],
...args: ComponentSelectExprArg<EmitContext>[]
): Component<EmitContext, Name> {
return {
name,
SQL: (ctx) => {
let condSql = "";
if (conditional) {
if ("anyExists" in conditional) {
condSql = `WHERE coalesce(${
Array.isArray(conditional.anyExists)
? conditional.anyExists.join(", ")
: conditional.anyExists
}, '') <> ''`;
} else if ("allExist" in conditional) {
condSql = `WHERE ${
Array.isArray(conditional.allExist)
? conditional.allExist.join(" and ")
: conditional.allExist
}`;
} else {
condSql = conditional.where.SQL(ctx);
}
}

const select = this.selectables(ctx, ...args);
// deno-fmt-ignore
return `SELECT '${name}' as component${select.length ? `, ${select.join(", ")}` : ""}`;
return `SELECT '${name}' as component${select.length ? `, ${select.join(", ")}` : ""}${condSql.length > 0 ? ` ${condSql}` : ''}`;
},
};
}
Expand All @@ -134,7 +158,7 @@ export class ComponentBuilder<
: Object.keys(args);
const sql = sqlBuilder(
args
? this.component(name, ...this.select(args, ...argNames))
? this.component(name, undefined, ...this.select(args, ...argNames))
: this.component(name),
);
return {
Expand Down Expand Up @@ -173,11 +197,30 @@ export interface Shell<EmitContext extends SQLa.SqlEmitContext>
readonly menuItems?: Iterable<{ caption: string }>;
}

export interface TextFragment<EmitContext extends SQLa.SqlEmitContext>
extends SQLa.SqlTextSupplier<EmitContext> {
readonly contents: string;
readonly bold?: boolean;
readonly break?: boolean;
readonly code?: boolean;
readonly color?: string;
readonly italics?: boolean;
readonly link?: string;
readonly size?: number;
readonly underline?: boolean;
}

export interface Text<EmitContext extends SQLa.SqlEmitContext>
extends Component<EmitContext> {
readonly name: "text";
readonly title: FlexibleText<EmitContext>;
readonly content: { readonly text: string } | { readonly markdown: string };
readonly content?:
| { readonly text: FlexibleText<EmitContext> }
| { readonly markdown: FlexibleText<EmitContext> }
| { readonly html: FlexibleText<EmitContext> };
readonly center?: boolean;
readonly width?: number;
readonly fragments?: Iterable<TextFragment<EmitContext>>;
}

export interface ListItem<
Expand Down Expand Up @@ -325,6 +368,7 @@ export function typicalComponents<
...init,
...builder.component(
"shell",
init.condition,
...builder.select(init, "title", "icon", "link"),
...(init.menuItems
? Array.from(init.menuItems).map((mi) =>
Expand All @@ -339,18 +383,31 @@ export function typicalComponents<
const text = (
init: Omit<Text<EmitContext>, "name" | "SQL">,
): Text<EmitContext> => {
return {
...init,
...builder.component(
"text",
...builder.select(init, "title"),
"text" in init.content ? [init.content.text, "contents"] : undefined,
"markdown" in init.content
? [init.content.markdown, "contentsmd"]
: undefined,
),
name: "text",
};
const { content } = init;
const topLevel = builder.component(
"text",
init.condition,
...builder.select(init, "title", "width", "center"),
content && "text" in content ? [content.text, "contents"] : undefined,
content && "markdown" in content
? [content.markdown, "contents_md"]
: undefined,
content && "html" in content ? [content.html, "html"] : undefined,
);
return init.fragments
? {
name: "text",
...init,
SQL: (ctx) => {
return `${topLevel.SQL(ctx)};\n` +
Array.from(init.fragments!).map((i) => i.SQL(ctx)).join(";\n");
},
}
: {
...init,
...topLevel,
name: "text",
};
};

const listItem = (
Expand All @@ -372,6 +429,7 @@ export function typicalComponents<
): List<EmitContext> => {
const topLevel = builder.component(
"list",
init.condition,
...builder.select(init, "title"),
);
return {
Expand Down Expand Up @@ -400,7 +458,8 @@ export function typicalComponents<
name: "table",
...init,
SQL: (ctx) => {
return `${builder.component("table", ...topLevel).SQL(ctx)};\n` +
// deno-fmt-ignore
return `${builder.component("table", init.condition, ...topLevel).SQL(ctx)};\n` +
Array.from(init.rows).map((i) => i.SQL(ctx)).join(";\n");
},
};
Expand Down
155 changes: 54 additions & 101 deletions pattern/sqlpage/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export function sqliteContent<EmitContext extends SQLa.SqlEmitContext>(
${tla.caption ? tla.caption : ""}
<ul>
{{#each_row}}
<li><a href="?${pp.table_nature}=${hc.nature}&${pp.table_name}=${hc.table_name}">${hc.table_name}</a> (${hc.columns_count})</li>
<li><a href="?${pp.table_nature}=${hc.nature}&${pp.table_name}=${hc.table_name}">${hc.table_name}</a> (${hc.nature}, ${hc.columns_count} columns)</li>
{{/each_row}}
</ul>`,
};
},
component: (tlaArg) => {
const tla = tlaArg ?? { caption: "Information Model (Schema) Tables" };
const tla = tlaArg ?? { caption: "Information Model Entities" };
return {
...tla,
...customCB.custom(
Expand All @@ -67,31 +67,60 @@ export function sqliteContent<EmitContext extends SQLa.SqlEmitContext>(
(topLevel) =>
SQL`
${topLevel}
SELECT
m.type as nature,
m.tbl_name AS ${rc.table_name},
(SELECT COUNT(*) FROM sqlite_master sm2 JOIN pragma_table_info(m.tbl_name) cc ON 1=1 WHERE sm2.tbl_name = m.tbl_name) AS ${rc.columns_count}
FROM sqlite_master m
ORDER BY table_name`,
SELECT m.type as nature, m.tbl_name AS ${rc.table_name}, (SELECT COUNT(*) FROM sqlite_master sm2 JOIN pragma_table_info(m.tbl_name) cc ON 1=1 WHERE sm2.tbl_name = m.tbl_name) AS ${rc.columns_count}
FROM sqlite_master m
ORDER BY table_name`,
),
};
},
};
return customComp;
}

const sqliteMaster = () => {
/**
* SQL page with self-refering links which which describes all the tables,
* columns, indexes, and views in the database.
* TODO: check out https://github.com/k1LoW/tbls and make this page equivalent
* to that utility's output including generating PlantUML through SQL.
*/
const infoSchemaSQL = () => {
const { text, table } = tc;
const imst = infoModelSchemaTables();

type PageParams = {
readonly table_nature: string;
readonly table_name: string;
};
type Row = {
readonly nature: string;
readonly table_name: string;
readonly columns_count: string;
};
// for type-safety:
// tla=top level args, pp=page params, rc=row column name, hc=handlebars colum name
const [pp, pv, rc] = [
comp.safePropNames<PageParams>(),
comp.safeUrlQueryParams<PageParams>(),
comp.safePropNames<Row>(),
];

const tableCond: comp.Component<EmitContext>["condition"] = {
anyExists: pv.table_name,
};

// deno-fmt-ignore
return SQL`
${text({ title: "Information Model (Schema) Documentation", content: { markdown: 'Test' }})}
${text({ title: "Information Model (Schema) Documentation", content: { markdown: 'TODO (description)' }})}
${imst.component()}
${table({rows: [{SQL: () => `
SELECT format('[%s](?${pp.table_name}=%s&${pp.table_nature})', m.tbl_name, m.tbl_name, m.type) AS ${rc.table_name},
(SELECT COUNT(*) FROM sqlite_master sm2 JOIN pragma_table_info(m.tbl_name) cc ON 1=1 WHERE sm2.tbl_name = m.tbl_name) AS ${rc.columns_count},
m.type as ${rc.nature}
FROM sqlite_master m
ORDER BY ${rc.table_name}
`}], columns: {[rc.table_name]: { markdown: true }}})}
${text({ title: { SQL: () => `($table_name || ' (' || $table_nature || ')' )` }, content: { markdown: 'Test' }})}
${table({ search: true, sort: true, rows: [
${text({ title: { SQL: () => `(${pv.table_name} || ' ' || ${pv.table_nature} || ' columns' )` }, content: { markdown: 'TODO (lineage, governance, description, etc.)' }, condition: tableCond })}
${table({ rows: [
{ SQL: () => `
SELECT
ROW_NUMBER() OVER (PARTITION BY m.tbl_name ORDER BY c.cid) AS column_num,
Expand All @@ -104,102 +133,26 @@ export function sqliteContent<EmitContext extends SQLa.SqlEmitContext>(
COALESCE((SELECT pfkl."table" || '.' || pfkl."to" FROM pragma_foreign_key_list(m.tbl_name) AS pfkl WHERE pfkl."from" = c.name), '') as fk_refs
-- TODO: add "is_indexed" and other details
FROM sqlite_master m JOIN pragma_table_info(m.tbl_name) c ON 1=1
WHERE m.tbl_name = $table_name`}]})}
WHERE m.tbl_name = ${pv.table_name}`}],
condition: tableCond })}
${text({ title: { SQL: () => `(${pv.table_name} || ' indexes')` }})}
${table({ rows: [
{ SQL: () => `
SELECT il.name as "Index Name", group_concat(ii.name, ', ') as columns
FROM sqlite_master as m, pragma_index_list(m.name) AS il, pragma_index_info(il.name) AS ii
WHERE m.tbl_name = ${pv.table_name}
GROUP BY m.name, il.name`}],
condition: tableCond })}
-- TODO: add indexes, views, etc. as emitted by tbls
-- TODO: add PlantUML or Mermaid ERD through SQL as emitted by tbls (use ChatGPT to create)
`;
};
/**
* SQL which generates the Markdown content lines (rows) which describes all
* the tables, columns, indexes, and views in the database. This should really
* be a view instead of a query but SQLite does not support use of pragma_* in
* views for security reasons.
* TODO: check out https://github.com/k1LoW/tbls and make this query equivalent
* to that utility's output including generating PlantUML through SQL.
*/
const infoSchemaSQL = () =>
SQL`
-- TODO: https://github.com/lovasoa/SQLpage/discussions/109#discussioncomment-7359513
-- see the above for how to fix for SQLPage but figure out to use the same SQL
-- in and out of SQLPage (maybe do what Ophir said in discussion and create
-- custom output for SQLPage using components?)
WITH TableInfo AS (
SELECT
m.tbl_name AS table_name,
CASE WHEN c.pk THEN '*' ELSE '' END AS is_primary_key,
c.name AS column_name,
c."type" AS column_type,
CASE WHEN c."notnull" THEN '*' ELSE '' END AS not_null,
COALESCE(c.dflt_value, '') AS default_value,
COALESCE((SELECT pfkl."table" || '.' || pfkl."to" FROM pragma_foreign_key_list(m.tbl_name) AS pfkl WHERE pfkl."from" = c.name), '') as fk_refs,
ROW_NUMBER() OVER (PARTITION BY m.tbl_name ORDER BY c.cid) AS row_num
FROM sqlite_master m JOIN pragma_table_info(m.tbl_name) c ON 1=1
WHERE m.type = 'table'
ORDER BY table_name, row_num
),
Views AS (
SELECT '## Views ' AS markdown_output
UNION ALL
SELECT '| View | Column | Type |' AS markdown_output
UNION ALL
SELECT '| ---- | ------ |----- |' AS markdown_output
UNION ALL
SELECT '| ' || tbl_name || ' | ' || c.name || ' | ' || c."type" || ' | '
FROM
sqlite_master m,
pragma_table_info(m.tbl_name) c
WHERE
m.type = 'view'
),
Indexes AS (
SELECT '## Indexes' AS markdown_output
UNION ALL
SELECT '| Table | Index | Columns |' AS markdown_output
UNION ALL
SELECT '| ----- | ----- | ------- |' AS markdown_output
UNION ALL
SELECT '| ' || m.name || ' | ' || il.name || ' | ' || group_concat(ii.name, ', ') || ' |' AS markdown_output
FROM sqlite_master as m,
pragma_index_list(m.name) AS il,
pragma_index_info(il.name) AS ii
WHERE
m.type = 'table'
GROUP BY
m.name,
il.name
)
SELECT
'text' as component,
'Information Schema' as title,
group_concat(markdown_output, '
') AS contents_md
FROM
(
SELECT '## Tables' AS markdown_output
UNION ALL
SELECT
CASE WHEN ti.row_num = 1 THEN '
### \`' || ti.table_name || '\` Table
| PK | Column | Type | Req? | Default | References |
| -- | ------ | ---- | ---- | ------- | ---------- |
' ||
'| ' || is_primary_key || ' | ' || ti.column_name || ' | ' || ti.column_type || ' | ' || ti.not_null || ' | ' || ti.default_value || ' | ' || ti.fk_refs || ' |'
ELSE
'| ' || is_primary_key || ' | ' || ti.column_name || ' | ' || ti.column_type || ' | ' || ti.not_null || ' | ' || ti.default_value || ' | ' || ti.fk_refs || ' |'
END
FROM TableInfo ti
UNION ALL SELECT ''
UNION ALL SELECT * FROM Views
UNION ALL SELECT ''
UNION ALL SELECT * FROM Indexes
);`;

return {
components: {
infoModelSchemaTables,
},
sqliteMaster,
infoSchemaSQL,
};
}
Loading

0 comments on commit a3e9318

Please sign in to comment.