# @theorchard/graphql-server

Common bootstrapping logic for running graphql servers.

## Usage

## Extending the types

The package exports extendable base types for config and context. You need to extend them in order to get your service types available in the resolvers and data-sources.

_src/types.ts_

```ts
import { ServiceConfig, ApolloContext, DataSources } from '@theorchard/graphql-server';

export interface ServiceConfig extends ServerConfig {
    someUrl: string;
    someFlag: boolean;
}

export interface ServiceContext extends ApolloContext {
    dataSources: ServiceDataSources;
}

export interface ServiceDataSources extends DataSources<ServiceContext> {
    owsSomething: OwsSomethingDataSource;
}
```

### Extending configuration

The package contains bootstrapping logic for loading common configuration options from the environment. It also provides a `env` util to load and validate your own config options.

_src/config.ts_

```ts
import { config, env } from '@theorchard/graphql-server';
import { ServiceConfig } from './types';

const serviceConfig: ServiceConfig = {
    ...config,
    someUrl: env.str('SOME_URL', 'fallback.com'),
    someFlag: env.bool('SOME_FLAG', false),
};

export default serviceConfig;
```

### Define your resolvers and data-sources

_src/connectors/index.ts_

```ts
import OwsSomethingDataSource from './owsSomethingDataSource';
import { ServiceDataSources } from '../types';
import config from '../config';

const createDataSources = (): ServiceDataSources => ({
    owsSomething: new OwsSomethingDataSource(config),
});

export default createDataSources;
```

_src/resolvers/index.ts_

```ts
import { TypeResolverMap } from '@theorchard/graphql-server';
import { ServiceContext } from '../types';

const resolvers: TypeResolverMap<ServiceContext> = {
    Query: {
        getSomething: (parent, args, context) =>
            context.dataSources.owsSomething.getSomething(args.id),
    },
};

export default resolvers;
```

### Start your server

The package exports several functions to configure and initialize express and apollo.
Most of the time though, you can just use the `startApolloServer` function. It creates an apollo-express server and applies the default middlewares and rules.

_src/server.ts_

```ts
import path from 'path';
import { startApolloServer } from '@theorchard/graphql-server';
import config from './config';
import resolvers from './resolvers';
import createDataSources from './connectors';

startApolloServer({
    config,
    resolvers,
    createDataSources,
});
```

### Extending the apollo context

Sometimes it make sense to extend the apollo context with common data for each request.
The `startApolloServer` accepts an extra callback that lets you modify the default context.

_src/server.ts_

```ts
startApolloServer({
    ...
    createContext: (baseContext, ({ req })) => {
        const { profile } = baseContext;
        return {
            ...baseContext,
            something: req.headers['something'],
            profileId: profile.profileId,
            profileType: profile.profileType,
            profileUUID: profile.profileUUID
        };
    })
});
```

### Adding more to the express app

If you want to modify the express application, the `startApolloServer` accepts an extra `initServer` callback.

_src/server.ts_

```ts
startApolloServer({
    ...
    initServer: (app, express) => {
        app.use(
            express.json({ limit: '2mb' })
        );
    }
});
```

---

## Utilities

The package exports several utilities to help with common tasks like validating inputs, logging and get info about your graphql queries.

### Input validation

Sometimes you can't trust the consumer to be respecting the types. e.g your code is executed in a js context.

**`assert<T>(value: T|null|undefined, name: string): T`**
Ensure that a value is not null or undefined.
Throws `"${name}" is null or undefined` if not.

**`assertProp<T>(obj: T|null|undefined, name: string): T`**
Ensure that a object property is not null or undefined.
Throws `"${name}" is null or undefined` if not.

```ts
import { assert, assertProp } from '@theorchard/graphql-server';

const doSomething = (value: string, options: { data: string }) => {
    assert(value, 'value');
    assertProp(options, 'data');
    ...
};
```

### Environment

**`env`**
Is an extended instance of [FieldValidator](/packages/field-validator) wrapped around the `process.env` object.
Great for validating/constructing your service config and determine the current environment.

```ts
import { env } from '@theorchard/graphql-server';

const config = {
    // mandatory field, throws if not defined
    snowflakeUserName: env.str('SF_USER_NAME'),
    // optional field, returns undefined if not defined
    redisUrl: env.optStr('REDIS_URL'),
    // optional field with default value
    enableRedis: env.bool('REDIS_ENABLED', false),
};
```

Note that the `env.bool` function parses common "boolean like" values.
`1`, `"1"`, `true`, `"TRUE"` and `"true"` are all considered boolean `true`.
**NOT including `"y"`, `"yes"` or similar.**

```ts
import { env } from '@theorchard/graphql-server';

const isThisForReal = (): string => {
    if (env.isProd) return 'yep, this better work';
    if (env.isQa) return 'almost';
    if (env.isDev) return 'only in your head';
    if (env.isTest) return 'not even close';
};

const isThisJustRunningLocally = (): boolean => {
    // same as isDev or isTest
    return env.isLocal;
};
```

### Logging

The package exports log functions for the common levels, `logDebug`, `logInfo`, `logWarning` and `logError`.
These functions will respect the environment variable `LOG_LEVEL`. If set to `info` (default), `logDebug` calls will be ignored.

```ts
import { logDebug, logInfo, logWarning, logError } from '@theorchard/graphql-server';

logDebug('if you love the gritty details');

logInfo('info people generally want to see');
logInfo({ data: 'this also works' });

logWarning('hey, its getting serious');

logError('ok, this is serious');
logError(new Error('this works as well'));
```

**`@timed`**
An decorator to log time spent in functions. **Note** it only outputs when `LOG_LEVEL` is set to `debug`.

```ts
import { timed } from '@theorchard/graphql-server';

@timed
function doesSomething() {
    ...
};
```

_outputs_

```bash
09:00:00 debug: Entering: "doesSomething"
09:00:10 debug: Exiting: "doesSomething": 10ms
```

---

## Sentry

Sentry is automatically enabled when you provide the `sentryDsn` config option or set the `SENTRY_DSN` env var.
IF you want to include graphql query and variables as part of the error report, you can enable it using the `sentryLogQuery` option or `SENTRY_LOG_QUERY` env var.

### Sensitive Data Sanitization

To automatically scrub sensitive information from Sentry error reports, you can provide a list of sensitive field names:

```ts
const config: ServiceConfig = {
    ...serverConfig,
    serviceName: SERVICE_NAME,
    serviceVersion: SERVICE_VERSION,
    sensitiveScrubFields: [
        'address_1',
        'address_2',
        'city',
        'province',
        'zip',
        'country_code',
        'tin',
        'first_name',
        'last_name',
        'date_of_birth',
        'email',
        'vat_number',
        'local_tax_id',
    ],
    ...
```

When `sensitiveScrubFields` is provided, the system will automatically:

-   Replace sensitive field values with `[Scrubbed]` before sending to Sentry
-   Process nested objects and arrays recursively
-   Scrub JSON strings in error messages and stack traces
-   Match field names case-insensitively

---

## Datadog tracing

To enable datadog tracing, you need to initialize it before loading any other instrumentation library.

```ts
// index.ts
import { init } from '@theorchard/graphql-server/datadog';
import { startApolloServer } from '@theorchard/graphql-server';

(async () => {
    // Need to await the init before continuing starting the server.
    await init({
        // If true (default), sets the version using the fargate task number and the git commit hash.
        // For local dev or with DD_TRACE_ENABLED=false, this value is ignored and the init skips fetching the version.
        loadVersion: true
    });

    startApolloServer( ... );
)();
```

You can later get a reference to the datadog tracer instance.

```ts
import { tracer } from '@theorchard/graphql-server/datadog';
```

---

## Advanced features

### Extending the apollo config

Sometimes you might want to tweak the apollo server config. Like adding plugins or changing cache control settings.
Check out the apollo docs for a full list of available options.
https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options

_src/server.ts_

```ts
startApolloServer({
    ...
    createConfig: (baseConfig) => ({
        ...baseConfig,
        plugins: [
            ...baseConfig.plugins ?? [],
            myOwnCoolCachePlugin
        ],
        cacheControl: {
            defaultMaxAge: 60 * 60,
            calculateHttpHeaders: false
        }
    })
});
```

### Composing your server

While the `startApolloServer` function usually provides all you need in one call, sometimes you just want more control.

_src/server.ts_

```ts
import path from 'path';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import {
    initApolloServer,
    initExpressServer,
    startExpressServer,
} from '@theorchard/graphql-server';
import config from './config';
import resolvers from './resolvers';
import createDataSources from './connectors';

const app = express();

// configures sentry, logs and health check endpoint
initExpressServer(app, config);

// configures apollo and adds the middleware
initApolloServer(app, {
    config,
    resolvers,
    createDataSources,

    // by default we look for your schema in the "src/schema" folder.
    // however, using the following option, lets you specify it.
    schemaLocation: path.join(__dirname, 'schema'),
});

// calls app.listen
startExpressServer(app, config).then(() => {
    console.log('started');
});
```

### Transforming the GraphQL schema

The `startApolloServer` options contain a property `schemaTransformers`, which is a list of functions that transform the schema. These can be used to apply [custom directives](https://www.apollographql.com/docs/apollo-server/schema/directives#custom-directives):

1. First you define the directive in the subgraph:

```gql
directive @upper on FIELD_DEFINITION | OBJECT

type TestType {
    testField: String! @upper
}
```

2. Then you define (or import) the transformer function- this example (taken from the [apollographql repo](https://github.com/apollographql/docs-examples/blob/main/apollo-server/v5/custom-directives/upper-case-directive/src/index.ts)) uppercases a resolved field:

```ts
import { getDirective, mapSchema } from '@graphql-tools/utils';

function upperDirectiveTransformer(schema) {
    return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const upperDirective = getDirective(schema, fieldConfig, 'upper')?.[0];
            if (upperDirective) {
                const { resolve = defaultFieldResolver } = fieldConfig;
                fieldConfig.resolve = async function (source, args, context, info) {
                    const result = await resolve(source, args, context, info);
                    if (typeof result === 'string') {
                        return result.toUpperCase();
                    }
                    return result;
                };
                return fieldConfig;
            }
        },
    });
}
```

3. Then you pass the function to `startApolloServer`:

```ts
await startApolloServer<ServiceConfig, ServiceContext>({
        ...,
        schemaTransformers: [upperDirectiveTransformer],
    });
```

**Please be very careful when applying multiple directives** to one field or type. `schemaTransformers` are applied in the same order as the array. Therefore, if you provide multiple `schemaTransformers` which modify the same part of the schema, you risk overwriting any previous changes, unless you explicitly handle this in your transformer functions.

**Please note that custom directives should be implemented consistently across subgraphs**. Composition does not detect or warn about inconsistencies (see [docs](https://www.apollographql.com/docs/apollo-server/schema/directives#in-subgraphs)), so please check the supergraph before implementing a new custom directive.
