Skip to content

Commit

Permalink
Add form_documents table and an initializeForm service, responsible f…
Browse files Browse the repository at this point in the history
…or creating a form that's initialized with data from a PDF. initializeForm is not currently wired to the UI.
  • Loading branch information
danielnaab committed Nov 1, 2024
1 parent 10bd089 commit ece3e1a
Show file tree
Hide file tree
Showing 18 changed files with 356 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function up(knex) {
await knex.schema.createTable('form_documents', table => {
table.uuid('id').primary();
table.string('type').notNullable();
table.string('file_name').notNullable();
table.binary('data').notNullable();
table.string('extract').notNullable();
});
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function down(knex) {
await knex.schema.dropTableIfExists('form_documents');
}
16 changes: 14 additions & 2 deletions packages/database/src/clients/kysely/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ColumnType,
Generated,
Insertable,
Kysely,
Expand All @@ -14,7 +15,9 @@ export interface Database<T extends Engine = Engine> {
sessions: SessionsTable<T>;
forms: FormsTable;
form_sessions: FormSessionsTable;
form_documents: FormDocumentsTable;
}
export type DatabaseClient = Kysely<Database>;

interface UsersTable {
id: string;
Expand Down Expand Up @@ -48,8 +51,6 @@ export type FormsTableSelectable = Selectable<FormsTable>;
export type FormsTableInsertable = Insertable<FormsTable>;
export type FormsTableUpdateable = Updateable<FormsTable>;

export type DatabaseClient = Kysely<Database>;

interface FormSessionsTable {
id: string;
form_id: string;
Expand All @@ -60,3 +61,14 @@ interface FormSessionsTable {
export type FormSessionsTableSelectable = Selectable<FormSessionsTable>;
export type FormSessionsTableInsertable = Insertable<FormSessionsTable>;
export type FormSessionsTableUpdateable = Updateable<FormSessionsTable>;

interface FormDocumentsTable {
id: string;
type: string;
data: ColumnType<Buffer, Buffer, Buffer>;
file_name: string;
extract: string;
}
export type FormDocumentsTableSelectable = Selectable<FormDocumentsTable>;
export type FormDocumentsTableInsertable = Insertable<FormDocumentsTable>;
export type FormDocumentsTableUpdateable = Updateable<FormDocumentsTable>;
13 changes: 12 additions & 1 deletion packages/forms/src/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
removePatternFromBlueprint,
updateFormSummary,
} from '../blueprint.js';
import { addDocument } from '../documents/document.js';
import { addDocument, addParsedPdfToForm } from '../documents/document.js';
import type { FormErrors } from '../error.js';
import {
createDefaultPattern,
Expand All @@ -23,6 +23,8 @@ import {
import { type FieldsetPattern } from '../patterns/fieldset/config.js';
import { type PageSetPattern } from '../patterns/page-set/config.js';
import type { Blueprint, FormSummary } from '../types.js';
import type { ParsedPdf } from '../documents/pdf/parsing-api.js';
import type { DocumentFieldMap } from '../documents/types.js';

export class BlueprintBuilder {
bp: Blueprint;
Expand All @@ -47,6 +49,15 @@ export class BlueprintBuilder {
this.bp = updatedForm;
}

async addDocumentRef(opts: { id: string; extract: ParsedPdf }) {
const { updatedForm } = await addParsedPdfToForm(this.form, {
id: opts.id,
label: opts.extract.title,
extract: opts.extract,
});
this.bp = updatedForm;
}

addPage() {
const newPage = createDefaultPattern(this.config, 'page');
this.bp = addPageToPageSet(this.form, newPage);
Expand Down
9 changes: 6 additions & 3 deletions packages/forms/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { type FormConfig } from '../pattern.js';
import { type FormRepository } from '../repository/index.js';
import type { ParsePdf } from '../documents/index.js';
import type { FormConfig } from '../pattern.js';
import type { FormRepository } from '../repository/index.js';

export { createTestBrowserFormService } from './test/index.js';
export { BrowserFormRepository } from './browser/form-repo.js';
export { createTestBrowserFormService } from './test/index.js';

export type FormServiceContext = {
repository: FormRepository;
config: FormConfig;
isUserLoggedIn: () => boolean;
parsePdf: ParsePdf;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { describe, expect, test } from 'vitest';
import { Success } from '@atj/common';

import { type DocumentFieldMap } from '../index.js';
import { fillPDF, getDocumentFieldData } from '../pdf/index.js';
import { fillPDF } from '../pdf/index.js';
import { getDocumentFieldData } from '../pdf/extract.js';

import { loadSamplePDF } from './sample-data.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/forms/src/documents/__tests__/extract.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import { getDocumentFieldData } from '../index.js';
import { loadSamplePDF } from './sample-data.js';
import { getDocumentFieldData } from '../pdf/extract.js';

describe('PDF form field extraction', () => {
it('extracts data from California UD-105 form', async () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/forms/src/documents/__tests__/fill-pdf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { beforeAll, describe, expect, it } from 'vitest';

import { type Failure, type Success } from '@atj/common';

import { getDocumentFieldData, fillPDF } from '../index.js';
import { fillPDF } from '../index.js';
import { loadSamplePDF } from './sample-data.js';
import { getDocumentFieldData } from '../pdf/extract.js';

describe('PDF form filler', () => {
let pdfBytes: Uint8Array;
Expand Down
35 changes: 33 additions & 2 deletions packages/forms/src/documents/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,47 @@ import { type Pattern } from '../pattern.js';
import { type InputPattern } from '../patterns/input/config.js';
import { type SequencePattern } from '../patterns/sequence.js';
import { type Blueprint } from '../types.js';
import { getDocumentFieldData } from './pdf/extract.js';

import { type PDFDocument, getDocumentFieldData } from './pdf/index.js';
import { type PDFDocument } from './pdf/index.js';
import {
type FetchPdfApiResponse,
processApiResponse,
type ParsedPdf,
fetchPdfApiResponse,
processApiResponse,
} from './pdf/parsing-api.js';

import { type DocumentFieldMap } from './types.js';

export type DocumentTemplate = PDFDocument;

export const addParsedPdfToForm = async (
form: Blueprint,
document: {
id: string;
label: string;
extract: ParsedPdf;
}
) => {
form = addPatternMap(form, document.extract.patterns, document.extract.root);
const updatedForm = addFormOutput(form, {
id: document.id,
data: new Uint8Array(), // TODO: remove this no-longer-used field
path: document.label,
fields: document.extract.outputs,
formFields: Object.fromEntries(
Object.keys(document.extract.outputs).map(output => {
return [output, document.extract.outputs[output].name];
})
),
});
return {
newFields: document.extract.outputs,
updatedForm,
errors: document.extract.errors,
};
};

export const addDocument = async (
form: Blueprint,
fileDetails: {
Expand All @@ -41,6 +70,7 @@ export const addDocument = async (
});
form = addPatternMap(form, parsedPdf.patterns, parsedPdf.root);
const updatedForm = addFormOutput(form, {
id: 'document-1', // TODO: generate a unique ID
data: fileDetails.data,
path: fileDetails.name,
fields: parsedPdf.outputs,
Expand All @@ -58,6 +88,7 @@ export const addDocument = async (
} else {
const formWithFields = addDocumentFieldsToForm(form, fields);
const updatedForm = addFormOutput(formWithFields, {
id: 'document-1', // TODO: generate a unique ID
data: fileDetails.data,
path: fileDetails.name,
fields,
Expand Down
20 changes: 19 additions & 1 deletion packages/forms/src/documents/pdf/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export { getDocumentFieldData } from './extract.js';
import { getDocumentFieldData } from './extract.js';
import {
type ParsedPdf,
fetchPdfApiResponse,
processApiResponse,
} from './parsing-api.js';
import type { DocumentFieldMap } from '../types.js';

export * from './generate.js';
export { generateDummyPDF } from './generate-dummy.js';

Expand All @@ -20,3 +27,14 @@ export type PDFFieldType =
| 'RadioGroup'
| 'Paragraph'
| 'RichText';

export type ParsePdf = (
pdf: Uint8Array
) => Promise<{ parsedPdf: ParsedPdf; fields: DocumentFieldMap }>;

export const parsePdf: ParsePdf = async (pdfBytes: Uint8Array) => {
const fields = await getDocumentFieldData(pdfBytes);
const apiResponse = await fetchPdfApiResponse(pdfBytes);
const parsedPdf = await processApiResponse(apiResponse);
return { parsedPdf, fields };
};
9 changes: 9 additions & 0 deletions packages/forms/src/documents/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import type { Result } from '@atj/common';

import { getDocumentFieldData } from './pdf/extract';
import {
type ParsedPdf,
fetchPdfApiResponse,
processApiResponse,
} from './pdf/parsing-api';

export type DocumentFieldValue =
| {
type: 'TextField';
Expand Down
29 changes: 29 additions & 0 deletions packages/forms/src/repository/add-document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { beforeAll, expect, it, vi } from 'vitest';

import { type DbTestContext, describeDatabase } from '@atj/database/testing';
import { addDocument } from './add-document.js';
import type { ParsedPdf } from '../documents/pdf/parsing-api.js';
import type { DocumentFieldMap } from '../documents/types.js';

describeDatabase('add document', () => {
const today = new Date(2000, 1, 1);

beforeAll(async () => {
vi.setSystemTime(today);
});

it<DbTestContext>('works', async ({ db }) => {
const result = await addDocument(db.ctx, {
fileName: 'file.pdf',
data: new Uint8Array([1, 2, 3]),
extract: {
parsedPdf: {} as ParsedPdf,
fields: {} as DocumentFieldMap,
},
});
if (result.success === false) {
expect.fail(`addDocument failed: ${result.error}`);
}
expect(result.data.id).toBeTypeOf('string');
});
});
39 changes: 39 additions & 0 deletions packages/forms/src/repository/add-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type Result, failure, success } from '@atj/common';
import { type DatabaseContext } from '@atj/database';

import type { ParsedPdf } from '../documents/pdf/parsing-api';
import type { DocumentFieldMap } from '../documents/types';

export type AddDocument = (
ctx: DatabaseContext,
document: {
fileName: string;
data: Uint8Array;
extract: {
parsedPdf: ParsedPdf;
fields: DocumentFieldMap;
};
}
) => Promise<Result<{ id: string }>>;

export const addDocument: AddDocument = async (ctx, document) => {
const uuid = crypto.randomUUID();
const db = await ctx.getKysely();

return await db
.insertInto('form_documents')
.values({
id: uuid,
type: 'pdf',
file_name: document.fileName,
data: Buffer.from(document.data),
extract: JSON.stringify(document.extract),
})
.execute()
.then(() =>
success({
id: uuid,
})
)
.catch(err => failure(err.message));
};
3 changes: 3 additions & 0 deletions packages/forms/src/repository/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ServiceMethod, createService } from '@atj/common';
import { type DatabaseContext } from '@atj/database';

import { type AddDocument, addDocument } from './add-document.js';
import { type AddForm, addForm } from './add-form.js';
import { type DeleteForm, deleteForm } from './delete-form.js';
import { type GetForm, getForm } from './get-form.js';
Expand All @@ -13,6 +14,7 @@ import {
} from './upsert-form-session.js';

export interface FormRepository {
addDocument: ServiceMethod<AddDocument>;
addForm: ServiceMethod<AddForm>;
deleteForm: ServiceMethod<DeleteForm>;
getForm: ServiceMethod<GetForm>;
Expand All @@ -24,6 +26,7 @@ export interface FormRepository {

export const createFormsRepository = (ctx: DatabaseContext): FormRepository =>
createService(ctx, {
addDocument,
addForm,
deleteForm,
getFormList,
Expand Down
5 changes: 4 additions & 1 deletion packages/forms/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createService, ServiceMethod } from '@atj/common';
import { type ServiceMethod, createService } from '@atj/common';

import { type FormServiceContext } from '../context/index.js';

Expand All @@ -7,6 +7,7 @@ import { type DeleteForm, deleteForm } from './delete-form.js';
import { type GetForm, getForm } from './get-form.js';
import { type GetFormList, getFormList } from './get-form-list.js';
import { type GetFormSession, getFormSession } from './get-form-session.js';
import { type InitializeForm, initializeForm } from './initialize-form.js';
import { type SaveForm, saveForm } from './save-form.js';
import { type SubmitForm, submitForm } from './submit-form.js';

Expand All @@ -17,6 +18,7 @@ export const createFormService = (ctx: FormServiceContext) =>
getForm,
getFormList,
getFormSession,
initializeForm,
saveForm,
submitForm,
});
Expand All @@ -27,6 +29,7 @@ export type FormService = {
getForm: ServiceMethod<GetForm>;
getFormList: ServiceMethod<GetFormList>;
getFormSession: ServiceMethod<GetFormSession>;
initializeForm: ServiceMethod<InitializeForm>;
saveForm: ServiceMethod<SaveForm>;
submitForm: ServiceMethod<SubmitForm>;
};
Loading

0 comments on commit ece3e1a

Please sign in to comment.