Skip to content

Commit

Permalink
feat(synced-files): allows automatic save of a .d.ts file storing typ…
Browse files Browse the repository at this point in the history
…es on creat/update/delete of collections, fields or relations

Add a hook saving types in files

List files to save to in an environment va

factorize data gathering between the admin module and the hook

closed maltejur#27
  • Loading branch information
jclaveau committed Jul 7, 2023
1 parent 6706fc3 commit e6a1ec9
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 62 deletions.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@
"entries": [
{
"type": "module",
"name": "generate-types-admin-module",
"source": "src/generate-types-admin-module/index.ts"
"name": "extension-module.admin",
"source": "src/extension-module.admin/index.ts"
},
{
"type": "hook",
"name": "extension-hook.sync-ts-files",
"source": "src/extension-hook.sync-ts-files/index.ts"
}
]
},
Expand Down
62 changes: 15 additions & 47 deletions src/generate-types-admin-module/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,27 @@
import type { Collections, Field } from "lib/types";
import type { Field } from "../../lib/types";
import {
Collection as DirectusCollection,
Relation,
} from "@directus/shared/types";
import type { AxiosResponse } from "axios";
import { warn } from "./console";
import { gatherCollectionsData } from "../../lib/generate-types/utils";

export async function getCollections(api) {
const collectionsRes: AxiosResponse<{ data: DirectusCollection[] }> =
await api.get("/collections?limit=-1");
const rawCollections = collectionsRes.data.data;
const collections: Collections = {};
rawCollections
.sort((a, b) => a.collection.localeCompare(b.collection))
.forEach(
(collection) =>
(collections[collection.collection] = { ...collection, fields: [] })
);
const fieldsRes: AxiosResponse<{ data: Field[] }> = await api.get(
const collectionsResponse: AxiosResponse<{ data: DirectusCollection[] }>
= await api.get("/collections?limit=-1");

const fieldsResponse: AxiosResponse<{ data: Field[] }> = await api.get(
"/fields?limit=-1"
);
const fields = fieldsRes.data.data;
fields
.sort((a, b) => a.field.localeCompare(b.field))
.forEach((field) => {
if (!collections[field.collection]) {
warn(`${field.collection} not found`);
return;
}
collections[field.collection].fields.push(field);
});
Object.keys(collections).forEach((key) => {
if (collections[key].fields.length === 0) delete collections[key];
});
const relationsRes: AxiosResponse<{ data: Relation[] }> = await api.get(

const relationsResponse: AxiosResponse<{ data: Relation[] }> = await api.get(
"/relations?limit=-1"
);
const relations = relationsRes.data.data;
relations.forEach((relation) => {
const oneField = collections[relation.meta.one_collection]?.fields.find(
(field) => field.field === relation.meta.one_field
);
const manyField = collections[relation.meta.many_collection]?.fields.find(
(field) => field.field === relation.meta.many_field
);
if (oneField)
oneField.relation = {
type: "many",
collection: relation.meta.many_collection,
};
if (manyField)
manyField.relation = {
type: "one",
collection: relation.meta.one_collection,
};
});
return collections;

return gatherCollectionsData(
collectionsResponse.data.data,
fieldsResponse.data.data,
relationsResponse.data.data,
);
}

2 changes: 1 addition & 1 deletion src/generate-types-admin-module/routes/oas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<script lang="ts">
import NavbarComponent from "../components/navigation.vue";
import CodeComponent from "../components/code.vue";
import generateOasTypes from "../lib/generateTypes/oas";
import generateOasTypes from "../../lib/generate-types/oas";
import languages from "../lib/languages";
export default {
Expand Down
5 changes: 3 additions & 2 deletions src/generate-types-admin-module/routes/py.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<script lang="ts">
import NavbarComponent from "../components/navigation.vue";
import CodeComponent from "../components/code.vue";
import generatePyTypes from "../lib/generateTypes/py";
import { getCollections } from "../lib/api";
import generatePyTypes from "../../lib/generate-types/py";
import languages from "../lib/languages";
export default {
Expand All @@ -26,7 +27,7 @@ export default {
};
},
mounted() {
generatePyTypes(this.api).then((types) => (this.types = types));
generatePyTypes(getCollections(this.api)).then((types) => (this.types = types));
},
};
</script>
Expand Down
5 changes: 3 additions & 2 deletions src/generate-types-admin-module/routes/ts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
<script lang="ts">
import NavbarComponent from "../components/navigation.vue";
import CodeComponent from "../components/code.vue";
import generateTsTypes from "../lib/generateTypes/ts";
import { getCollections } from "../lib/api";
import generateTsTypes from "../../lib/generate-types/ts";
import languages from "../lib/languages";
export default {
Expand Down Expand Up @@ -65,7 +66,7 @@ const directus = new Directus<CustomDirectusTypes>("<directus url>");`,
"directus-extension-generate-types-use-intersection-types",
this.useIntersectionTypes
);
generateTsTypes(this.api, this.useIntersectionTypes).then((types) => {
generateTsTypes(getCollections(this.api), this.useIntersectionTypes).then((types) => {
this.types = types;
this.loading = false;
});
Expand Down
79 changes: 79 additions & 0 deletions src/generate-types-sync-ts-files/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { defineHook } from '@directus/extensions-sdk';
import generateTsTypes from "../lib/generate-types/ts";
import { ActionHandler, Collection, Field, Relation, SchemaOverview } from "@directus/types";
import { Collections } from '../lib/types';
import { gatherCollectionsData } from '../lib/generate-types/utils';
import * as fs from 'node:fs';

export default defineHook(({ action }, extCtx) => {
const { services: { CollectionsService, FieldsService, RelationsService }, env, logger } = extCtx;

let targetFiles: string | string[] = env.GENERATE_TYPES_SYNCED_TS_FILES;

if (targetFiles == null || targetFiles.length === 0) {
logger.info('No target file defined to automatically sync TypeScript types')
return
}

if (! Array.isArray(targetFiles)) {
targetFiles = [targetFiles]
}

const listCollections = async (schema: SchemaOverview) => {
const collectionsService = new CollectionsService({schema});
const collections: Collection[] = await collectionsService.readByQuery();

const fieldsService = new FieldsService({schema});
const fields: Field[] = await fieldsService.readAll();

const relationsService = new RelationsService({schema});
const relations: Relation[] = await relationsService.readAll();

const collectionsData: Collections = await gatherCollectionsData(
collections,
fields,
relations
)

let useIntersectionTypes = false;
generateTsTypes(collectionsData, useIntersectionTypes).then((types) => {
targetFiles.forEach((targetFile: string) => {
writeToFile('./', targetFile, types)
logger.info(`Types synced into ${targetFile}`)
})
});
}

const onChange: ActionHandler = async ({ }, { schema }) => {
if (schema === null) {
throw new Error('schema is null');
}
listCollections(schema);
}

action('collections.create', onChange);
action('collections.update', onChange);
action('collections.delete', onChange);

action('fields.create', onChange);
action('fields.update', onChange);
action('fields.delete', onChange);

action('relations.create', onChange);
action('relations.update', onChange);
action('relations.delete', onChange);
});


export function writeToFile(directoryPath: string, fileName: string, data: string) {
try {
fs.mkdirSync(directoryPath, {recursive: true})
}
catch (e: any) {
if (e.code != `EEXIST`) {
throw e
}
}

fs.writeFileSync(`${directoryPath}/${fileName}`, data)
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { getCollections } from "../api";
import { Collections } from "../types";

export default async function generatePyTypes(api) {
const collections = await getCollections(api);
export default async function generatePyTypes(collections: Collections | Promise<Collections>) {
let ret = "";
const types = [];

if (collections instanceof Promise) {
collections = await collections;
}

ret += `from typing import TypedDict\n\n`;

ret += Object.values(collections)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Field } from "lib/types";
import { getCollections } from "../api";
import { Collections, Field } from '../types';

export default async function generateTsTypes(
api,
collections: Collections | Promise<Collections>,
useIntersectionTypes = false
) {
const collections = await getCollections(api);
let ret = "";
const types = [];

if (collections instanceof Promise) {
collections = await collections;
}

Object.values(collections).forEach((collection) => {
const collectionName = collection.collection;
const typeName = pascalCase(collectionName);
Expand Down
50 changes: 50 additions & 0 deletions src/lib/generate-types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Collections } from "../types";
import { warn } from "../console";

export async function gatherCollectionsData(rawCollections, rawFields , rawRelations) {

const collections: Collections = {};
rawCollections
.sort((a, b) => a.collection.localeCompare(b.collection))
.forEach(
(collection) =>
(collections[collection.collection] = { ...collection, fields: [] })
);

rawFields
.sort((a, b) => a.field.localeCompare(b.field))
.forEach((field) => {
if (!collections[field.collection]) {
warn(`${field.collection} not found`);
return;
}
collections[field.collection].fields.push(field);
});

Object.keys(collections).forEach((key) => {
if (collections[key].fields.length === 0) delete collections[key];
});

rawRelations.forEach((relation) => {
const oneField = collections[relation.meta.one_collection]?.fields.find(
(field) => field.field === relation.meta.one_field
);
const manyField = collections[relation.meta.many_collection]?.fields.find(
(field) => field.field === relation.meta.many_field
);
if (oneField)
oneField.relation = {
type: "many",
collection: relation.meta.many_collection,
};
if (manyField)
manyField.relation = {
type: "one",
collection: relation.meta.one_collection,
};
});

return collections;
}


File renamed without changes.
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"rootDir": "./src"
"rootDir": "./src",
"sourceMap": true,
// baseUrl has no effect?
"baseUrl": "./src"
},
"include": ["./src/**/*.ts"]
}

0 comments on commit e6a1ec9

Please sign in to comment.