Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Typecheck

on:
pull_request:

jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun i
- name: Run typecheck
run: bun typecheck
16 changes: 13 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 97 additions & 45 deletions lib/abilityBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type Span, trace } from "@opentelemetry/api";
import { relationsFilterToSQL } from "drizzle-orm";
import { debounce } from "es-toolkit";
import { lazy } from "./helpers/lazy";
Expand Down Expand Up @@ -115,6 +116,7 @@ export const createAbilityBuilder = <
db,
actions,
defaultLimit,
otel,
}: RumbleInput<UserContext, DB, RequestEvent, Action, PothosConfig>) => {
type TableNames = keyof DrizzleQueryFunction<DB>;

Expand Down Expand Up @@ -548,59 +550,109 @@ export const createAbilityBuilder = <
withContext: (userContext: UserContext) => {
return {
filter: (action: Action) => {
const filters = queryFilters.get(action);

// in case we have a wildcard ability, skip the rest and return no filters at all
if (filters === "unrestricted") {
return transformToResponse();
}
const assembleAbilities = (span?: Span) => {
const filters = queryFilters.get(action);

// in case we have a wildcard ability, skip the rest and return no filters at all
if (filters === "unrestricted") {
span?.setAttribute("abilities.status", "unrestricted");
const r = transformToResponse();
span?.end();
return r;
}

// if nothing has been allowed, block everything
if (!filters) {
nothingRegisteredWarningLogger(
tableName.toString(),
action,
);
return transformToResponse(blockEverythingFilter);
}
// if nothing has been allowed, block everything
if (!filters) {
span?.setAttribute(
"abilities.status",
"blocked_everything",
);
nothingRegisteredWarningLogger(
tableName.toString(),
action,
);
const r = transformToResponse(blockEverythingFilter);
span?.end();
return r;
}

// run all dynamic filters
const dynamicResults = new Array<
DrizzleQueryFunctionInput<DB, TableName>
>(dynamicQueryFilters[action].length);
let filtersReturned = 0;
for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
const func = dynamicQueryFilters[action][i];
const result = func(userContext);
// if one of the dynamic filters returns "allow", we want to allow everything
if (result === "allow") {
return transformToResponse();
// run all dynamic filters
const dynamicResults = new Array<
DrizzleQueryFunctionInput<DB, TableName>
>(dynamicQueryFilters[action].length);
let filtersReturned = 0;
for (
let i = 0;
i < dynamicQueryFilters[action].length;
i++
) {
const func = dynamicQueryFilters[action][i];
const result = func(userContext);
// if one of the dynamic filters returns "allow", we want to allow everything
if (result === "allow") {
const r = transformToResponse();
span?.end();
return r;
}
// if nothing is returned, nothing is allowed by this filter
if (result === undefined) continue;

dynamicResults[filtersReturned++] = result;
}
// if nothing is returned, nothing is allowed by this filter
if (result === undefined) continue;
dynamicResults.length = filtersReturned;

dynamicResults[filtersReturned++] = result;
}
dynamicResults.length = filtersReturned;
span?.setAttribute(
"abilities.dynamic",
dynamicResults.length,
);
span?.setAttribute(
"abilities.static",
simpleQueryFilters[action].length,
);

const allQueryFilters = [
...simpleQueryFilters[action],
...dynamicResults,
];
const allQueryFilters = [
...simpleQueryFilters[action],
...dynamicResults,
];

// if we don't have any permitted filters then block everything
if (allQueryFilters.length === 0) {
return transformToResponse(blockEverythingFilter);
}
span?.setAttribute(
"abilities.total",
allQueryFilters.length,
);

const mergedFilters =
allQueryFilters.length === 1
? allQueryFilters[0]
: allQueryFilters.reduce((a, b) => {
return mergeFilters(a, b);
}, {});
// if we don't have any permitted filters then block everything
if (allQueryFilters.length === 0) {
span?.setAttribute(
"abilities.status",
"blocked_everything",
);

return transformToResponse(mergedFilters as any);
const r = transformToResponse(blockEverythingFilter);
span?.end();
return r;
}

const mergedFilters =
allQueryFilters.length === 1
? allQueryFilters[0]
: allQueryFilters.reduce((a, b) => {
return mergeFilters(a, b);
}, {});

span?.setAttribute("abilities.status", "applied");
const r = transformToResponse(mergedFilters as any);
span?.end();
return r;
};

if (otel?.tracer) {
return otel.tracer.startActiveSpan(
`prepare_query_abilities_${action}`,
assembleAbilities,
);
} else {
return assembleAbilities();
}
},
};
},
Expand Down
81 changes: 57 additions & 24 deletions lib/rumble.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
import { AttributeNames, SpanNames } from "@pothos/tracing-opentelemetry";
import { merge } from "es-toolkit";
import {
createYoga as nativeCreateYoga,
type Plugin,
type YogaServerOptions,
} from "graphql-yoga";
import { useSofa } from "sofa-api";
Expand Down Expand Up @@ -61,7 +63,7 @@ export const db = drizzle(
"postgres://postgres:postgres@localhost:5432/postgres",
{
relations, // <--- add this line
schema,
schema,
},
);

Expand Down Expand Up @@ -230,6 +232,37 @@ export const db = drizzle(
...(enableApiDocs
? []
: [useDisableIntrospection(), EnvelopArmorPlugin()]),
rumbleInput.otel?.tracer
? ({
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) =>
rumbleInput.otel!.tracer.startActiveSpan(
SpanNames.EXECUTE,
{
attributes: {
[AttributeNames.OPERATION_NAME]:
options.operationName ?? undefined,
// TODO
// [AttributeNames.SOURCE]: print(options.document),
},
},
async (span) => {
try {
const result = await executeFn(options);

return result;
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
},
),
);
},
} as Plugin)
: false,
].filter(Boolean),
schema: builtSchema(),
context,
Expand Down Expand Up @@ -264,18 +297,18 @@ export const db = drizzle(
return {
/**
* The ability builder. Use it to declare whats allowed for each entity in your DB.
*
*
* @example
*
*
* ```ts
* // users can edit themselves
abilityBuilder.users
.allow(["read", "update", "delete"])
.when(({ userId }) => ({ where: eq(schema.users.id, userId) }));

// everyone can read posts
abilityBuilder.posts.allow("read");
*
*
* ```
*/
abilityBuilder,
Expand All @@ -285,32 +318,32 @@ export const db = drizzle(
schemaBuilder,
/**
* Creates the native yoga instance. Can be used to run an actual HTTP server.
*
*
* @example
*
*
* ```ts
*
*
import { createServer } from "node:http";
* const server = createServer(createYoga());
server.listen(3000, () => {
* const server = createServer(createYoga());
server.listen(3000, () => {
console.info("Visit http://localhost:3000/graphql");
});
* ```
* https://the-guild.dev/graphql/yoga-server/docs#server
});
* ```
* https://the-guild.dev/graphql/yoga-server/docs#server
*/
createYoga,
/**
* Creates a sofa instance to offer a REST API.
```ts
import express from 'express';
const app = express();
const sofa = createSofa(...);
app.use('/api', useSofa({ schema }));
```
* https://the-guild.dev/graphql/sofa-api/docs#usage
*/
* Creates a sofa instance to offer a REST API.
```ts
import express from 'express';

const app = express();
const sofa = createSofa(...);

app.use('/api', useSofa({ schema }));
```
* https://the-guild.dev/graphql/sofa-api/docs#usage
*/
createSofa,
/**
* A function for creating default objects for your schema
Expand Down
8 changes: 4 additions & 4 deletions lib/runtimeFiltersPlugin/pluginTypes.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SchemaTypes } from "@pothos/core";
import type { Tracer } from "@opentelemetry/api";
import type { pluginName } from "./filterTypes";
import type {
applyFiltersKey,
Expand All @@ -11,10 +12,9 @@ declare global {
[pluginName]: RuntimeFiltersPlugin<Types>;
}

// export interface SchemaBuilderOptions<Types extends SchemaTypes> {
// optionInRootOfConfig?: boolean;
// nestedOptionsObject?: ExamplePluginOptions;
// }
export interface SchemaBuilderOptions<Types extends SchemaTypes> {
otelTracer?: Tracer;
}

// export interface BuildSchemaOptions<Types extends SchemaTypes> {
// customBuildTimeOptions?: boolean;
Expand Down
Loading