-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-google-docs): add field to upload to google docs
- Loading branch information
1 parent
30ee6f7
commit 0c227d3
Showing
17 changed files
with
1,322 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
Large diffs are not rendered by default.
Oops, something went wrong.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* eslint-disable import/no-relative-packages */ | ||
import jestConfig from '../../jest.config'; | ||
|
||
export default { | ||
...jestConfig, | ||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'], | ||
testMatch: ['<rootDir>/test/**/*.test.ts'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "@forestadmin/plugin-google-docs", | ||
"version": "1.0.0", | ||
"main": "dist/index.js", | ||
"license": "GPL-3.0", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/ForestAdmin/agent-nodejs.git", | ||
"directory": "packages/plugin-google-docs" | ||
}, | ||
"dependencies": { | ||
"@googleapis/docs": "^2.0.1", | ||
"@googleapis/drive": "^5.1.0" | ||
}, | ||
"devDependencies": { | ||
"@forestadmin/datasource-customizer": "1.7.1", | ||
"@forestadmin/datasource-toolkit": "1.5.0" | ||
}, | ||
"files": [ | ||
"dist/**/*.js", | ||
"dist/**/*.d.ts" | ||
], | ||
"scripts": { | ||
"build": "tsc", | ||
"build:watch": "tsc --watch", | ||
"clean": "rm -rf coverage dist", | ||
"lint": "eslint src test", | ||
"test": "jest" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { Configuration } from '../types'; | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
|
||
import { encodeDataUri } from '../utils/data-uri'; | ||
|
||
export default function createField(collection: CollectionCustomizer, config: Configuration): void { | ||
collection.addField(config.filename, { | ||
columnType: 'String', // TODO check type for config.readMode === 'download' | ||
dependencies: [config.sourcename], | ||
getValues: records => | ||
records.map(async record => { | ||
const key = record[config.sourcename]; | ||
|
||
if (!key) { | ||
return null; | ||
} | ||
|
||
if (config.readMode === 'title') { | ||
return (await config.client.load(key)).title; | ||
} | ||
|
||
if (config.readMode === 'download') { | ||
return encodeDataUri({ | ||
mimeType: 'application/pdf', | ||
buffer: await config.client.exportToPdf(key), | ||
}); | ||
} | ||
|
||
if (config.readMode === 'webViewLink') { | ||
return (await config.client.getFileFromDrive(key)).webViewLink; | ||
} | ||
|
||
if (config.readMode === 'webContentLink') { | ||
return (await config.client.getFileFromDrive(key)).webContentLink; | ||
} | ||
|
||
return key; | ||
}), | ||
}); | ||
} |
18 changes: 18 additions & 0 deletions
18
packages/plugin-google-docs/src/field/make-field-filterable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Configuration } from '../types'; | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
import type { ColumnSchema } from '@forestadmin/datasource-toolkit'; | ||
|
||
export default function makeFieldFilterable( | ||
collection: CollectionCustomizer, | ||
config: Configuration, | ||
): void { | ||
const schema = collection.schema.fields[config.sourcename] as ColumnSchema; | ||
|
||
for (const operator of schema.filterOperators) { | ||
collection.replaceFieldOperator(config.filename, operator, value => ({ | ||
field: config.sourcename, | ||
operator, | ||
value, | ||
})); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
packages/plugin-google-docs/src/field/make-field-required.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { Configuration } from '../types'; | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
import type { ColumnSchema } from '@forestadmin/datasource-toolkit'; | ||
|
||
export default function makeFieldRequired( | ||
collection: CollectionCustomizer, | ||
config: Configuration, | ||
): void { | ||
const schema = collection.schema.fields[config.sourcename] as ColumnSchema; | ||
|
||
if (schema.validation?.find(rule => rule.operator === 'Present')) { | ||
collection.addFieldValidation(config.filename, 'Present'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { Configuration } from '../types'; | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
|
||
export default function makeFieldSortable( | ||
collection: CollectionCustomizer, | ||
config: Configuration, | ||
): void { | ||
collection.replaceFieldSorting(config.filename, [{ ascending: true, field: config.sourcename }]); | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/plugin-google-docs/src/field/make-field-writable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
import type { ColumnSchema, RecordData } from '@forestadmin/datasource-toolkit'; | ||
|
||
import { Configuration } from '../types'; | ||
|
||
function getRecordId(collection: CollectionCustomizer, record: RecordData): string[] { | ||
return Object.entries(collection.schema.fields) | ||
.filter(([, schema]) => schema.type === 'Column' && schema.isPrimaryKey) | ||
.map(([name]) => record[name]); | ||
} | ||
|
||
export default function makeFieldWritable( | ||
collection: CollectionCustomizer, | ||
config: Configuration, | ||
): void { | ||
const schema = collection.schema.fields[config.sourcename] as ColumnSchema; | ||
if (schema.isReadOnly) return; | ||
|
||
collection.replaceFieldWriting(config.filename, async (value, context) => { | ||
const patch = { [config.sourcename]: null }; | ||
|
||
// Auto provision at creation only ? | ||
if (value) { | ||
const recordId = getRecordId(collection, context.record).join('|'); | ||
|
||
const documentId = await config.client.create(recordId); | ||
|
||
patch[config.sourcename] = documentId; | ||
} | ||
|
||
return patch; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { Configuration } from '../types'; | ||
import type { CollectionCustomizer } from '@forestadmin/datasource-customizer'; | ||
|
||
export default function replaceField( | ||
collection: CollectionCustomizer, | ||
config: Configuration, | ||
): void { | ||
collection.removeField(config.sourcename); | ||
collection.renameField(config.filename, config.sourcename); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import type { | ||
CollectionCustomizer, | ||
DataSourceCustomizer, | ||
} from '@forestadmin/datasource-customizer'; | ||
|
||
import createField from './field/create-field'; | ||
import makeFieldFilterable from './field/make-field-filterable'; | ||
import makeFieldRequired from './field/make-field-required'; | ||
import makeFieldSortable from './field/make-field-sortable'; | ||
import makeFieldWritable from './field/make-field-writable'; | ||
import replaceField from './field/replace-field'; | ||
import { Options } from './types'; | ||
import Client from './utils/google'; | ||
|
||
export { Options }; | ||
|
||
export async function createGoogleDocsField( | ||
_dataSource: DataSourceCustomizer, | ||
collection: CollectionCustomizer, | ||
options?: Options, | ||
): Promise<void> { | ||
if (!collection) throw new Error('createGoogleDocsField can only be used on collections.'); | ||
if (!options) throw new Error('Options must be provided.'); | ||
|
||
const sourceSchema = collection.schema.fields[options.fieldname]; | ||
|
||
if (!sourceSchema || sourceSchema.type !== 'Column' || sourceSchema.columnType !== 'String') { | ||
const field = `${collection.name}.${options.fieldname}`; | ||
throw new Error(`The field '${field}' does not exist or is not a string.`); | ||
} | ||
|
||
const config = { | ||
readMode: options.readMode ?? 'download', | ||
sourcename: options.fieldname, | ||
filename: `${options.fieldname}__google-docs`, | ||
client: new Client(options.google), | ||
}; | ||
|
||
createField(collection, config); | ||
makeFieldWritable(collection, config); | ||
makeFieldSortable(collection, config); | ||
makeFieldFilterable(collection, config); | ||
makeFieldRequired(collection, config); | ||
replaceField(collection, config); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* eslint-disable max-len */ | ||
|
||
import Client from './utils/google'; | ||
|
||
/** | ||
* Configuration for the GOOGLE docs addon of Forest Admin. | ||
* | ||
* TODO: specify required GOOGLE authorization scopes | ||
* ['https://www.googleapis.com/auth/documents', 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.appdata', 'https://www.googleapis.com/auth/drive.appfolder', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/drive.resource', 'https://www.googleapis.com/auth/drive.metadata'] | ||
*/ | ||
export type Options = { | ||
/** Name of the field that you want to use as a file-picker on the frontend */ | ||
fieldname: string; | ||
|
||
/** | ||
*/ | ||
readMode?: 'title' | 'download' | 'webViewLink' | 'webContentLink' | 'raw'; | ||
|
||
/** GOOGLE configuration */ | ||
google?: { | ||
/** GOOGLE API keys, defaults to process.env.GOOGLE_API_KEY. */ | ||
auth?: string; | ||
|
||
/** Alternatively, you can specify the path to the GOOGLE service account credential file via the keyFile */ | ||
// keyFile?: string; | ||
}; | ||
}; | ||
|
||
export type Configuration = Required< | ||
Pick<Options, 'readMode'> & { | ||
client: Client; | ||
sourcename: string; | ||
filename: string; | ||
} | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export function encodeDataUri(data: { mimeType: string; buffer: Buffer }): string { | ||
// prefix | ||
let uri = `data:${data.mimeType}`; | ||
|
||
// media types | ||
const mediaTypes = Object.entries(data) | ||
.filter(([mediaType, value]) => value && mediaType !== 'mimeType' && mediaType !== 'buffer') | ||
.map(([mediaType, value]) => `${mediaType}=${encodeURIComponent(value as string)}`) | ||
.join(';'); | ||
|
||
if (mediaTypes.length) uri += `;${mediaTypes}`; | ||
|
||
// data | ||
uri += `;base64,${data.buffer.toString('base64')}`; | ||
|
||
return uri; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { docs, docs_v1 } from '@googleapis/docs'; | ||
import { drive, drive_v3 } from '@googleapis/drive'; | ||
|
||
import { Options } from '../types'; | ||
|
||
export default class Client { | ||
private client: docs_v1.Docs; | ||
private service: drive_v3.Drive; | ||
|
||
constructor(options: Options['google']) { | ||
this.client = docs({ version: 'v1', auth: options.auth }); | ||
this.service = drive({ version: 'v3', auth: options.auth }); | ||
} | ||
|
||
async exportToPdf(fileId: string): Promise<Buffer> { | ||
// https://developers.google.com/drive/api/reference/rest/v3/files/export | ||
const buffer = await this.service.files.export({ | ||
fileId, | ||
mimeType: 'application/pdf', | ||
}); | ||
|
||
return buffer as unknown as Buffer; | ||
} | ||
|
||
async getFileFromDrive(fileId: string) { | ||
// https://developers.google.com/drive/api/reference/rest/v3/files/get | ||
const file = await this.service.files.get({ | ||
fileId, | ||
}); | ||
|
||
return file.data; | ||
} | ||
|
||
async load(documentId: string): Promise<docs_v1.Schema$Document> { | ||
const response = await this.client.documents.get({ | ||
documentId, | ||
}); | ||
|
||
return response.data; | ||
} | ||
|
||
async updates(documentId: string, text: string): Promise<void> { | ||
await this.client.documents.batchUpdate({ | ||
documentId, | ||
requestBody: { | ||
requests: [ | ||
{ | ||
insertText: { | ||
// The first text inserted into the document must create a paragraph, | ||
// which can't be done with the `location` property. Use the | ||
// `endOfSegmentLocation` instead, which assumes the Body if | ||
// unspecified. | ||
endOfSegmentLocation: {}, | ||
text, | ||
}, | ||
}, | ||
], | ||
}, | ||
}); | ||
} | ||
|
||
async create(originId: string): Promise<string> { | ||
// The initial call to create the doc will have a title but no content. | ||
// This is a limitation of the underlying API. | ||
const createResponse = await this.client.documents.create({ | ||
requestBody: { | ||
title: `Your new document for ${originId}!`, | ||
}, | ||
}); | ||
|
||
return createResponse.data.documentId; | ||
} | ||
} |
Oops, something went wrong.