Skip to content

Commit

Permalink
Add dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Synvox committed May 23, 2023
1 parent 96fca78 commit 018ef19
Show file tree
Hide file tree
Showing 3 changed files with 413 additions and 23 deletions.
73 changes: 71 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ await sql`

## Nested Resources

Eager load related data using `nest` and `nestFirst` helpers:
Eager load related data using `nestAll` and `nestFirst` helpers:

```ts
await sql`
Expand All @@ -172,12 +172,81 @@ await sql`
`.nestFirst()} as post
from test.post_likes
where post_likes.user_id = users.id
`.nest()} as liked_posts
`.nestAll()} as liked_posts
from test.users
where users.id = ${user.id}
`.first();
```

## Dependencies and Policies

If you would like to access a table but through a policy, you may use a dependency:

```ts
async function users(ctx) {
const team = await getTeam(ctx);
return dependency(
"users",
sql`
select *
from users
where team_id = ${team.id}
and deleted_at is null
`,
{ mode: "not materialized" }
);
}

async function projects(ctx) {
const team = await getTeam(ctx);
return dependency(
"projects",
sql`
select *
from projects
where team_id = ${team.id}
`
);
}

const projects = await sql`
select
projects.*
${sql`
select users.email
from ${await users(ctx)}
where projects.user_id = users.id
`} as contact_email
from ${await projects(ctx)}
`;
```

Dependencies are transformed to common table expressions. This example would run:

```sql
with
users as (
select *
from users
where team_id = $1
and deleted_at is null
),
projects as (
select *
from projects
where team_id = $2
) (
select
projects.*,
(
select users.email
from users
where projects.user_id = users.id
) as contact_email
from projects
)
```

## Transactions

```ts
Expand Down
103 changes: 87 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,26 @@ const debugTransaction = Debug("sql:transaction");
const debugError = Debug("sql:error");

const statementWeakSet = new WeakSet<Statement>();
const dependencyWeakSet = new WeakSet<Dependency>();

export function isStatement(anything: any): anything is Statement {
return anything && statementWeakSet.has(anything);
}

export function isDependency(anything: any): anything is Dependency {
return anything && dependencyWeakSet.has(anything);
}

export function dependency<Name extends string>(
name: Name,
statement: Statement,
options: { mode?: "materialized" | "not materialized" } = {}
) {
const dep = { name, statement, options };
dependencyWeakSet.add(dep);
return dep;
}

type Value = any;

type InterpolatedValue =
Expand All @@ -27,20 +42,22 @@ type InterpolatedValue =
interface StatementState {
text: string;
values: Value[];
dependents: Dependency[];
}

interface Statement {
text: string;
values: Value[];
interface Statement extends StatementState {
toNative: () => { text: string; values: Value[] };
exec: () => Promise<QueryResult>;
paginate: (page?: number, per?: number) => Promise<unknown[]>;
nest: () => Statement;
nestAll: () => Statement;
nestFirst: () => Statement;
all: <T>() => Promise<T[]>;
first: <T>() => Promise<T | undefined>;
compile: () => string;
}

type Dependency = ReturnType<typeof dependency>;

type Options = {
caseMethod: "snake" | "camel" | "constant" | "pascal" | "none";
depth: number;
Expand Down Expand Up @@ -77,8 +94,40 @@ function makeSql(
): Statement {
const builder = Object.assign({}, state, {
toNative() {
const segments = state.text.split(/\?/i);
const text = segments
let values: any[] = [];

let text = state.text;
if (Object.keys(state.dependents).length) {
let dependents: Dependency[] = [];

let walk = (dep: Dependency) => {
const preexisting = dependents.find((v) => v.name === dep.name);
if (!preexisting) {
dependents.push(dep);
} else if (preexisting.statement.text !== dep.statement.text) {
throw new Error(`Conflicting dependency name: ${dep.name}`);
}

dep.statement.dependents.forEach(walk);
};
state.dependents.forEach(walk);

text = `with ${dependents
.map((dependency) => {
const { mode } = dependency.options;
const { text, values: v } = dependency.statement;
const key = dependency.name;
const pragma = mode ? `${mode} ` : "";
values.push(...v);
return `${key} as ${pragma}(${text})`;
})
.join(", ")} (${state.text})`;
}

values.push(...state.values);
const segments = text.split(/\?/i);
text = segments
.map((segment, index) => {
if (index + 1 === segments.length) return segment;
return `${segment}$${index + 1}`;
Expand All @@ -89,7 +138,7 @@ function makeSql(

return {
text,
values: state.values,
values,
};
},
async exec() {
Expand All @@ -108,14 +157,17 @@ function makeSql(
async paginate(page: number = 0, per: number = 250) {
page = Math.max(0, page);

const dep: Dependency = dependency("paginated", builder, {
mode: "not materialized",
});

const paginated = sql`
with stmt as not materialized (${builder}) select * from stmt limit ${per} offset ${page *
per}
select paginated.* from ${dep} limit ${per} offset ${page * per}
`;

return paginated.all();
},
nest() {
nestAll() {
return sql`coalesce((select jsonb_agg(subquery) as nested from (${builder}) subquery), '[]'::jsonb)`;
},
nestFirst() {
Expand All @@ -130,9 +182,9 @@ function makeSql(
return result[0];
},
compile() {
const args = state.values;
return state.text.replace(/\?/g, () =>
escapeLiteral(String(args.shift()))
const { text, values } = this.toNative();
return text.replace(/\$\d/g, () =>
escapeLiteral(String(values.shift()))
);
},
});
Expand All @@ -149,6 +201,7 @@ function makeSql(
let state: StatementState = {
text: "",
values: [],
dependents: [],
};

const toStatement = (value: InterpolatedValue) => {
Expand All @@ -158,6 +211,7 @@ function makeSql(
const statement = Statement({
text: "?",
values: [value],
dependents: [],
});
return statement;
}
Expand All @@ -183,6 +237,18 @@ function makeSql(
if (isStatement(arg)) {
state.text += arg.text;
state.values.push(...arg.values);
for (let dep of arg.dependents) {
state.dependents.push(dep);
}
}

// dependencies
else if (isDependency(arg)) {
for (let dep of arg.statement.dependents) {
state.dependents.push(dep);
}
state.dependents.push(arg);
state.text += arg.name;
}

// arrays
Expand Down Expand Up @@ -367,9 +433,14 @@ function makeSql(
const sql = Object.assign(unboundSql, {
transaction,
connection,
identifier: (identifier: string) =>
Statement({ text: escapeIdentifier(identifier), values: [] }),
literal: (value: any) => Statement({ text: "?", values: [value] }),
ref: (identifier: string) =>
Statement({
text: escapeIdentifier(identifier),
values: [],
dependents: [],
}),
literal: (value: any) =>
Statement({ text: "?", values: [value], dependents: [] }),
});

return sql;
Expand Down
Loading

0 comments on commit 018ef19

Please sign in to comment.