Skip to content

Commit

Permalink
feat(hasura): metadata v3 support (#284)
Browse files Browse the repository at this point in the history
fixes #250
  • Loading branch information
WonderPanda committed Jul 6, 2021
1 parent 7a031ce commit bcb6fc6
Show file tree
Hide file tree
Showing 36 changed files with 571 additions and 77 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
7 changes: 7 additions & 0 deletions packages/hasura/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
test/__fixtures__/hasura/v2/metadata/tables.yaml
test/__fixtures__/hasura/v2/metadata/cron_triggers.yaml

test/__fixtures__/hasura/v3/metadata/cron_triggers.yaml
test/__fixtures__/hasura/v3/metadata/databases/additional/tables/public_additional_table.yaml
test/__fixtures__/hasura/v3/metadata/databases/default/tables/public_default_table.yaml
test/__fixtures__/hasura/v3/metadata/databases/additional/tables/public_additional_table.yaml
6 changes: 4 additions & 2 deletions packages/hasura/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"@golevelup/nestjs-discovery": "^2.3.1",
"@golevelup/nestjs-modules": "^0.4.2",
"@hasura/metadata": "^1.0.2",
"js-yaml": "^3.14.1"
"js-yaml": "^4.1.0",
"zod": "^3.3.4"
},
"jest": {
"moduleFileExtensions": [
Expand All @@ -58,6 +59,7 @@
},
"gitHead": "6f97aab8ce9d65dc074750a3ee467ec5ff3b9908",
"devDependencies": {
"@types/js-yaml": "^3.12.5"
"@types/js-yaml": "^3.12.5",
"ts-toolbelt": "^9.6.0"
}
}
9 changes: 9 additions & 0 deletions packages/hasura/src/hasura.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export interface ScheduledEventRetryConfig extends EventRetryConfig {
}

export interface TrackedHasuraEventHandlerConfig {
/**
* Only necessary to provide this value if working with Metadata V3. Defaults to 'default'
*/
databaseName?: string;
schema?: string;
tableName: string;
triggerName: string;
Expand Down Expand Up @@ -134,6 +138,11 @@ export interface HasuraModuleConfig {
* amount of boilerplate
*/
managedMetaDataConfig?: {
/**
* The version of hasura metadata being targeted
*/
metadataVersion?: 'v2' | 'v3';

/**
* The ENV key in which Hasura will store the secret header value used to validate event payloads
*/
Expand Down
140 changes: 71 additions & 69 deletions packages/hasura/src/hasura.metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,40 @@ import {
TrackedHasuraEventHandlerConfig,
TrackedHasuraScheduledEventHandlerConfig,
} from './hasura.interfaces';
import { safeLoad, safeDump } from 'js-yaml';
import { load, dump } from 'js-yaml';
import { readFileSync, writeFileSync } from 'fs';
import { orderBy } from 'lodash';
import {
TableEntry,
EventTriggerDefinition,
Columns,
CronTrigger,
} from './hasura-metadata-dist/HasuraMetadataV2';
import { mergeEventHandlerConfig } from './metadata/event-triggers';

const utf8 = 'utf-8';

const MISSING_META_CONFIG = 'No configuration for meta available';

const defaultHasuraRetryConfig: ScheduledEventRetryConfig = {
intervalInSeconds: 10,
numRetries: 3,
timeoutInSeconds: 60,
toleranceSeconds: 21600,
};

const convertEventTriggerDefinition = (
configDef: TrackedHasuraEventHandlerConfig['definition']
): EventTriggerDefinition => {
if (configDef.type === 'insert') {
return {
enable_manual: false,
insert: {
columns: Columns.Empty,
},
};
}

if (configDef.type === 'delete') {
return {
enable_manual: false,
delete: {
columns: Columns.Empty,
},
};
}

return {
enable_manual: false,
update: {
columns: configDef.columns ?? Columns.Empty,
},
};
};

export const isTrackedHasuraEventHandlerConfig = (
eventHandlerConfig: HasuraEventHandlerConfig | TrackedHasuraEventHandlerConfig
): eventHandlerConfig is TrackedHasuraEventHandlerConfig => {
return 'definition' in eventHandlerConfig;
};

export const updateEventTriggerMeta = (
const updateEventTriggerMetaV2 = (
moduleConfig: HasuraModuleConfig,
eventHandlerConfigs: TrackedHasuraEventHandlerConfig[]
) => {
const { managedMetaDataConfig } = moduleConfig;

if (!managedMetaDataConfig) {
throw new Error('No configuration for meta available');
throw new Error(MISSING_META_CONFIG);
}

const defaultRetryConfig =
Expand All @@ -75,16 +47,11 @@ export const updateEventTriggerMeta = (
const tablesYamlPath = `${managedMetaDataConfig.dirPath}/tables.yaml`;

const tablesMeta = readFileSync(tablesYamlPath, utf8);
const tableEntries = safeLoad(tablesMeta) as TableEntry[];
const tableEntries = load(tablesMeta) as TableEntry[];

orderBy(eventHandlerConfigs, (x) => x.triggerName).forEach((config) => {
const {
schema = 'public',
tableName,
triggerName,
definition,
retryConfig = defaultRetryConfig,
} = config;
const { schema = 'public', tableName } = config;

const matchingTable = tableEntries.find(
(x) => x.table.schema === schema && x.table.name === tableName
);
Expand All @@ -95,50 +62,85 @@ export const updateEventTriggerMeta = (
);
}

const { intervalInSeconds, numRetries, timeoutInSeconds } = retryConfig;
const eventTriggers = (matchingTable.event_triggers ?? []).filter(
(x) => x.name !== triggerName
matchingTable.event_triggers = mergeEventHandlerConfig(
config,
moduleConfig,
defaultRetryConfig,
matchingTable
);

matchingTable.event_triggers = [
...eventTriggers,
{
name: triggerName,
definition: convertEventTriggerDefinition(definition),
retry_conf: {
num_retries: numRetries,
interval_sec: intervalInSeconds,
timeout_sec: timeoutInSeconds,
},
webhook_from_env: managedMetaDataConfig.nestEndpointEnvName,
headers: [
{
name: moduleConfig.webhookConfig.secretHeader,
value_from_env: managedMetaDataConfig.secretHeaderEnvName,
},
],
},
];
});

const yamlString = safeDump(tableEntries);
const yamlString = dump(tableEntries);
writeFileSync(tablesYamlPath, yamlString, utf8);
};

const updateEventTriggerMetaV3 = (
moduleConfig: HasuraModuleConfig,
eventHandlerConfigs: TrackedHasuraEventHandlerConfig[]
) => {
const { managedMetaDataConfig } = moduleConfig;

if (!managedMetaDataConfig) {
throw new Error(MISSING_META_CONFIG);
}

const defaultRetryConfig =
managedMetaDataConfig.defaultEventRetryConfig ?? defaultHasuraRetryConfig;

eventHandlerConfigs.forEach((config) => {
const { schema = 'public', databaseName = 'default', tableName } = config;

const tableYamlPath = `${managedMetaDataConfig.dirPath}/databases/${databaseName}/tables/${schema}_${tableName}.yaml`;
const tableMeta = readFileSync(tableYamlPath, utf8);
const tableEntry = load(tableMeta) as TableEntry;

tableEntry.event_triggers = mergeEventHandlerConfig(
config,
moduleConfig,
defaultRetryConfig,
tableEntry
);

const yamlString = dump(tableEntry);
writeFileSync(tableYamlPath, yamlString, utf8);
});
};

export const updateEventTriggerMeta = (
moduleConfig: HasuraModuleConfig,
eventHandlerConfigs: TrackedHasuraEventHandlerConfig[]
) => {
const { managedMetaDataConfig } = moduleConfig;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { metadataVersion = 'v2' } = moduleConfig.managedMetaDataConfig!;

if (!managedMetaDataConfig) {
throw new Error(MISSING_META_CONFIG);
}

if (metadataVersion === 'v2') {
updateEventTriggerMetaV2(moduleConfig, eventHandlerConfigs);
} else if (metadataVersion === 'v3') {
updateEventTriggerMetaV3(moduleConfig, eventHandlerConfigs);
} else {
throw new Error(`Invalid Hasura metadata version: ${metadataVersion}`);
}
};

export const updateScheduledEventTriggerMeta = (
moduleConfig: HasuraModuleConfig,
scheduledEventHandlerConfigs: TrackedHasuraScheduledEventHandlerConfig[]
) => {
const { managedMetaDataConfig } = moduleConfig;

if (!managedMetaDataConfig) {
throw new Error('No configuration for meta available');
throw new Error(MISSING_META_CONFIG);
}

const cronTriggersYamlPath = `${managedMetaDataConfig.dirPath}/cron_triggers.yaml`;

const cronTriggersMeta = readFileSync(cronTriggersYamlPath, utf8);
const cronEntries = (safeLoad(cronTriggersMeta) ?? []) as CronTrigger[];
const cronEntries = (load(cronTriggersMeta) ?? []) as CronTrigger[];

const managedCronTriggerNames = scheduledEventHandlerConfigs.map(
(x) => x.name
Expand Down Expand Up @@ -183,6 +185,6 @@ export const updateScheduledEventTriggerMeta = (
...managedCronTriggers,
];

const yamlString = safeDump(newCronEntries);
const yamlString = dump(newCronEntries);
writeFileSync(cronTriggersYamlPath, yamlString, utf8);
};
76 changes: 76 additions & 0 deletions packages/hasura/src/metadata/event-triggers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Columns,
EventTriggerDefinition,
TableEntry,
} from '../hasura-metadata-dist/HasuraMetadataV2';
import {
HasuraModuleConfig,
ScheduledEventRetryConfig,
TrackedHasuraEventHandlerConfig,
} from '../hasura.interfaces';

const convertEventTriggerDefinition = (
configDef: TrackedHasuraEventHandlerConfig['definition']
): EventTriggerDefinition => {
if (configDef.type === 'insert') {
return {
enable_manual: false,
insert: {
columns: Columns.Empty,
},
};
}

if (configDef.type === 'delete') {
return {
enable_manual: false,
delete: {
columns: Columns.Empty,
},
};
}

return {
enable_manual: false,
update: {
columns: configDef.columns ?? Columns.Empty,
},
};
};

export const mergeEventHandlerConfig = (
config: TrackedHasuraEventHandlerConfig,
moduleConfig: HasuraModuleConfig,
defaultRetryConfig: ScheduledEventRetryConfig,
existingTableEntry: TableEntry
): TableEntry['event_triggers'] => {
const { managedMetaDataConfig } = moduleConfig;

const { triggerName, definition, retryConfig = defaultRetryConfig } = config;

const eventTriggers = (existingTableEntry.event_triggers ?? []).filter(
(x) => x.name !== triggerName
);

const { intervalInSeconds, numRetries, timeoutInSeconds } = retryConfig;

return [
...eventTriggers,
{
name: triggerName,
definition: convertEventTriggerDefinition(definition),
retry_conf: {
num_retries: numRetries,
interval_sec: intervalInSeconds,
timeout_sec: timeoutInSeconds,
},
webhook_from_env: managedMetaDataConfig?.nestEndpointEnvName,
headers: [
{
name: moduleConfig.webhookConfig.secretHeader,
value_from_env: managedMetaDataConfig?.secretHeaderEnvName,
},
],
},
];
};
Empty file.
Loading

0 comments on commit bcb6fc6

Please sign in to comment.