googleformsubmitter 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-main.yml +33 -0
- package/package.json +7 -2
- package/src/google-form-submitter.test.ts +38 -5
- package/src/google-form-submitter.ts +28 -0
- package/tools/introspect-form.ts +264 -0
- package/.data/form-cache-1FAIpQLSc1aiEAhxWCMSNjZ1asGDtXV_mT8vTiJRquiqfdjsoMkKT9tw.json +0 -16
- package/CLAUDE.md +0 -106
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish main branch
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for OIDC https://docs.npmjs.com/trusted-publishers
|
|
10
|
+
contents: write # Required for pushing tags
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish-main:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20
|
|
21
|
+
registry-url: https://registry.npmjs.org/
|
|
22
|
+
# Ensure npm 11.5.1 or later is installed
|
|
23
|
+
- run: npm install -g npm@latest
|
|
24
|
+
- name: Bump version
|
|
25
|
+
run: |
|
|
26
|
+
git config --global user.name "github-actions"
|
|
27
|
+
git config --global user.email "github-actions@github.com"
|
|
28
|
+
npm version patch
|
|
29
|
+
- name: Publish to NPM
|
|
30
|
+
run: npm publish --access public
|
|
31
|
+
- name: Commit and push
|
|
32
|
+
run: |
|
|
33
|
+
git push --follow-tags
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "googleformsubmitter",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/iconicompany/googleformsubmitter.git"
|
|
11
|
+
},
|
|
8
12
|
"scripts": {
|
|
9
13
|
"test": "bun test",
|
|
10
14
|
"lint": "oxlint",
|
|
11
15
|
"validate": "tsgo --noEmit",
|
|
12
|
-
"create-form": "bun run ./tools/create-form.ts"
|
|
16
|
+
"create-form": "bun run ./tools/create-form.ts",
|
|
17
|
+
"introspect-form": "bun run ./tools/introspect-form.ts"
|
|
13
18
|
},
|
|
14
19
|
"devDependencies": {
|
|
15
20
|
"@types/bun": "latest",
|
|
@@ -84,7 +84,7 @@ describe('GoogleFormSubmitter E2E Integration', () => {
|
|
|
84
84
|
|
|
85
85
|
const formUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSc1aiEAhxWCMSNjZ1asGDtXV_mT8vTiJRquiqfdjsoMkKT9tw/viewform';
|
|
86
86
|
const credentialsPath = path.join(__dirname, '..', '..', 'imatching', '.data', 'credentials.json');
|
|
87
|
-
const tokenPath = path.join(__dirname, '..', '..', 'imatching', '.data', '
|
|
87
|
+
const tokenPath = path.join(__dirname, '..', '..', 'imatching', '.data', 'gdrive-auth.json');
|
|
88
88
|
|
|
89
89
|
submitter = new GoogleFormSubmitter({
|
|
90
90
|
formUrl,
|
|
@@ -103,10 +103,10 @@ describe('GoogleFormSubmitter E2E Integration', () => {
|
|
|
103
103
|
test('should dynamically resolve fields, upload file, and submit successfully', async () => {
|
|
104
104
|
const candidateData = {
|
|
105
105
|
requestId: '910',
|
|
106
|
-
fullName: '
|
|
106
|
+
fullName: 'Тестов Тест (Схема)',
|
|
107
107
|
birthDate: '1996-07-03',
|
|
108
108
|
grade: 'Middle',
|
|
109
|
-
location: 'Россия,
|
|
109
|
+
location: 'Россия, Москва',
|
|
110
110
|
rate: '2000',
|
|
111
111
|
nds: 'Нет',
|
|
112
112
|
exitDate: 'через 21 дней',
|
|
@@ -114,7 +114,7 @@ describe('GoogleFormSubmitter E2E Integration', () => {
|
|
|
114
114
|
inStaff: 'Да',
|
|
115
115
|
cvFile: {
|
|
116
116
|
buffer: Buffer.from('mock cv file contents for integration test'),
|
|
117
|
-
filename: 'cv_
|
|
117
|
+
filename: 'cv_Тестов_Тест.docx',
|
|
118
118
|
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
119
119
|
},
|
|
120
120
|
company: 'Iconicompany',
|
|
@@ -130,7 +130,7 @@ describe('GoogleFormSubmitter E2E Integration', () => {
|
|
|
130
130
|
expect(result1.statusCode).toBe(200);
|
|
131
131
|
|
|
132
132
|
console.log('\n--- SECOND RUN: Submitting instantly using cache ---');
|
|
133
|
-
candidateData.fullName = '
|
|
133
|
+
candidateData.fullName = 'Тестов Тест (Схема-Кэш)';
|
|
134
134
|
const result2 = await submitter.submit(candidateData);
|
|
135
135
|
console.log('Result 2 Fields Submitted:', JSON.stringify(result2.fieldsSubmitted, null, 2));
|
|
136
136
|
|
|
@@ -138,3 +138,36 @@ describe('GoogleFormSubmitter E2E Integration', () => {
|
|
|
138
138
|
expect(result2.statusCode).toBe(200);
|
|
139
139
|
}, 45000); // 45 seconds timeout
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
describe('GoogleFormSubmitter.formatFields Unit Tests', () => {
|
|
143
|
+
test('should format simple text and choice fields correctly', () => {
|
|
144
|
+
const fields = {
|
|
145
|
+
'Номер запроса': '917',
|
|
146
|
+
'ФИО (полностью)': 'Иванов Иван Иванович',
|
|
147
|
+
'Грейд': 'middle'
|
|
148
|
+
};
|
|
149
|
+
const formatted = GoogleFormSubmitter.formatFields(fields);
|
|
150
|
+
expect(formatted).toContain('• Номер запроса: 917');
|
|
151
|
+
expect(formatted).toContain('• ФИО (полностью): Иванов Иван Иванович');
|
|
152
|
+
expect(formatted).toContain('• Грейд: middle');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should parse nested CV array and display only the filename', () => {
|
|
156
|
+
const fields = {
|
|
157
|
+
'CV (файл)': '[[["1Nrch5iArW-Gx-Vz4x9YlAM6FuhI5tEtt","cv_Иванов_Иван_Иванович.docx","application/vnd.openxmlformats-officedocument.wordprocessingml.document"]]]'
|
|
158
|
+
};
|
|
159
|
+
const formatted = GoogleFormSubmitter.formatFields(fields);
|
|
160
|
+
expect(formatted).toBe('• CV (файл): cv_Иванов_Иван_Иванович.docx');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should handle empty/null/undefined values gracefully', () => {
|
|
164
|
+
const fields = {
|
|
165
|
+
'Комментарий': null,
|
|
166
|
+
'Ближайший планируемый отпуск': undefined
|
|
167
|
+
};
|
|
168
|
+
const formatted = GoogleFormSubmitter.formatFields(fields);
|
|
169
|
+
expect(formatted).toContain('• Комментарий: —');
|
|
170
|
+
expect(formatted).toContain('• Ближайший планируемый отпуск: —');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
@@ -337,4 +337,32 @@ export class GoogleFormSubmitter {
|
|
|
337
337
|
await browser.close();
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Helper to format submitted fields or raw payload into a clean, human-readable list.
|
|
343
|
+
*/
|
|
344
|
+
public static formatFields(fields: Record<string, any>): string {
|
|
345
|
+
const formatValue = (val: any): string => {
|
|
346
|
+
if (typeof val === 'string') {
|
|
347
|
+
if (val.startsWith('[[[') && val.endsWith(']]]')) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(val);
|
|
350
|
+
if (Array.isArray(parsed) && parsed[0]?.[0]?.[1]) {
|
|
351
|
+
return parsed[0][0][1];
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
return val;
|
|
356
|
+
}
|
|
357
|
+
if (val === null || val === undefined) {
|
|
358
|
+
return '—';
|
|
359
|
+
}
|
|
360
|
+
return String(val);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return Object.entries(fields)
|
|
364
|
+
.map(([key, val]) => `• ${key}: ${formatValue(val)}`)
|
|
365
|
+
.join('\n');
|
|
366
|
+
}
|
|
340
367
|
}
|
|
368
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
function toCamelCase(str: string): string {
|
|
6
|
+
// Map common Russian terms to standard project English keys
|
|
7
|
+
const translations: Record<string, string> = {
|
|
8
|
+
'фио': 'fullName',
|
|
9
|
+
'дата рождения': 'birthDate',
|
|
10
|
+
'грейд': 'grade',
|
|
11
|
+
'локация': 'location',
|
|
12
|
+
'рейт': 'rate',
|
|
13
|
+
'ндс': 'nds',
|
|
14
|
+
'когда кандидат': 'exitDate',
|
|
15
|
+
'отпуск': 'vacation',
|
|
16
|
+
'штате': 'inStaff',
|
|
17
|
+
'cv': 'cvFile',
|
|
18
|
+
'резюме': 'cvFile',
|
|
19
|
+
'компания': 'company',
|
|
20
|
+
'контакты': 'contacts',
|
|
21
|
+
'требованиям': 'checklist',
|
|
22
|
+
'комментарий': 'comment',
|
|
23
|
+
'номер запроса': 'requestId'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const lower = str.toLowerCase();
|
|
27
|
+
for (const [key, val] of Object.entries(translations)) {
|
|
28
|
+
if (lower.includes(key)) {
|
|
29
|
+
return val;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback to transliteration + camelCase
|
|
34
|
+
const clean = str
|
|
35
|
+
.replace(/[^a-zA-Zа-яА-Я0-9\s]/g, '')
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\s+/g, ' ');
|
|
38
|
+
|
|
39
|
+
const ru: Record<string, string> = {
|
|
40
|
+
'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', 'з':'z', 'и':'i', 'й':'y', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'ts', 'ч':'ch', 'ш':'sh', 'щ':'sch', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', 'я':'ya',
|
|
41
|
+
'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', 'З':'Z', 'И':'I', 'Й':'Y', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'Ts', 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sch', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', 'Я':'Ya'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let transliterated = '';
|
|
45
|
+
for (const char of clean) {
|
|
46
|
+
const replacement = ru[char] !== undefined ? ru[char] : char;
|
|
47
|
+
transliterated += replacement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const words = transliterated.split(' ');
|
|
51
|
+
return words
|
|
52
|
+
.map((word, idx) => {
|
|
53
|
+
if (idx === 0) return word.toLowerCase();
|
|
54
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
55
|
+
})
|
|
56
|
+
.join('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const url = process.argv[2];
|
|
61
|
+
if (!url) {
|
|
62
|
+
console.error('Usage: bun run ./tools/introspect-form.ts <google-form-url>');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`🔍 Connecting to Google Form at: ${url}...`);
|
|
67
|
+
|
|
68
|
+
let browser;
|
|
69
|
+
try {
|
|
70
|
+
// Attempt connecting to local Lightpanda first
|
|
71
|
+
browser = await chromium.connectOverCDP(process.env.LIGHTPANDA_CDP_URL || 'ws://127.0.0.1:9222/', { timeout: 5000 });
|
|
72
|
+
} catch {
|
|
73
|
+
console.log('Lightpanda not found, falling back to local headless Chromium...');
|
|
74
|
+
browser = await chromium.launch({ headless: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const context = await browser.newContext();
|
|
78
|
+
|
|
79
|
+
// Load session cookies if GOOGLE_COOKIES_PATH is configured
|
|
80
|
+
const cookiesPath = process.env.GOOGLE_COOKIES_PATH;
|
|
81
|
+
if (cookiesPath) {
|
|
82
|
+
try {
|
|
83
|
+
const cookiesStr = await fs.readFile(cookiesPath, 'utf8');
|
|
84
|
+
const cookies = JSON.parse(cookiesStr);
|
|
85
|
+
await context.addCookies(cookies);
|
|
86
|
+
console.log(`[Introspect] Injected ${cookies.length} session cookies from ${cookiesPath}`);
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
console.warn(`[Introspect] Warning: Could not inject cookies: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const page = await context.newPage();
|
|
93
|
+
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await page.goto(url);
|
|
97
|
+
|
|
98
|
+
// Wait for the form contents to load
|
|
99
|
+
await Promise.any([
|
|
100
|
+
page.locator('.Qr7Oae').first().waitFor({ state: 'attached', timeout: 15000 }),
|
|
101
|
+
page.waitForURL(/\/closedform/, { timeout: 15000 }).catch(() => {})
|
|
102
|
+
]).catch(() => {});
|
|
103
|
+
|
|
104
|
+
if (page.url().includes('/closedform') || (await page.locator('form').count() === 0)) {
|
|
105
|
+
throw new Error('This Google Form is closed or redirecting to login. Make sure your session cookies are valid.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Inspect DOM
|
|
109
|
+
const questions = await page.evaluate(() => {
|
|
110
|
+
const g = globalThis as any;
|
|
111
|
+
const result: any[] = [];
|
|
112
|
+
const containers = g.document.querySelectorAll('.Qr7Oae');
|
|
113
|
+
|
|
114
|
+
containers.forEach((container: any) => {
|
|
115
|
+
const headerEl = container.querySelector('[role="heading"], .M7eMe, .HoRgec');
|
|
116
|
+
if (!headerEl) return;
|
|
117
|
+
|
|
118
|
+
let labelText = (headerEl.textContent || '').trim();
|
|
119
|
+
const isRequired = labelText.endsWith('*');
|
|
120
|
+
labelText = labelText.replace(/\s*\*$/, '').trim();
|
|
121
|
+
|
|
122
|
+
let type = 'text';
|
|
123
|
+
let choices: string[] = [];
|
|
124
|
+
let allowOther = false;
|
|
125
|
+
|
|
126
|
+
const paramsEl = container.hasAttribute('data-params') ? container : container.querySelector('[data-params]');
|
|
127
|
+
if (paramsEl) {
|
|
128
|
+
const dataParams = paramsEl.getAttribute('data-params');
|
|
129
|
+
try {
|
|
130
|
+
// Replace Google Forms prefix with '[' to restore the outer array opening bracket
|
|
131
|
+
const jsonText = dataParams.replace(/^%\.@\./, '[');
|
|
132
|
+
const parsed = JSON.parse(jsonText);
|
|
133
|
+
const questionInfo = parsed[0];
|
|
134
|
+
const typeId = questionInfo[3];
|
|
135
|
+
|
|
136
|
+
if (typeId === 11 || typeId === 13) {
|
|
137
|
+
type = 'file';
|
|
138
|
+
} else if (typeId === 9) {
|
|
139
|
+
type = 'date';
|
|
140
|
+
} else if (typeId === 1) {
|
|
141
|
+
type = 'paragraph';
|
|
142
|
+
} else if (typeId === 2 || typeId === 3 || typeId === 4) {
|
|
143
|
+
type = 'choice';
|
|
144
|
+
const choicesArray = questionInfo[4]?.[0]?.[1];
|
|
145
|
+
if (choicesArray && Array.isArray(choicesArray)) {
|
|
146
|
+
choices = choicesArray.map((c: any) => c[0]).filter((val) => val !== null && val !== undefined && val !== '');
|
|
147
|
+
}
|
|
148
|
+
allowOther = !!questionInfo[4]?.[0]?.[2];
|
|
149
|
+
}
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
console.error('PAGE LOG [Parser Error]:', e.message, 'Data:', dataParams);
|
|
152
|
+
// Fallback to legacy DOM checks if parse fails
|
|
153
|
+
if (container.querySelector('[type="file"]') || container.querySelector('[data-params*="file"]')) {
|
|
154
|
+
type = 'file';
|
|
155
|
+
} else if (container.querySelector('input[type="date"]') || container.querySelector('[data-params*="[[3,"]')) {
|
|
156
|
+
type = 'date';
|
|
157
|
+
} else if (container.querySelector('[role="radio"], [role="checkbox"], select, .SGZTVe')) {
|
|
158
|
+
type = 'choice';
|
|
159
|
+
const optionEls = container.querySelectorAll('[role="radio"] + *, [role="checkbox"] + *, option, .fwW70c, .text');
|
|
160
|
+
optionEls.forEach((opt: any) => {
|
|
161
|
+
const text = (opt.textContent || '').trim();
|
|
162
|
+
if (text && !text.toLowerCase().includes('другое') && !text.toLowerCase().includes('other')) {
|
|
163
|
+
choices.push(text);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else if (container.querySelector('textarea')) {
|
|
167
|
+
type = 'paragraph';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.warn('PAGE LOG: paramsEl not found for label:', labelText);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.push({
|
|
175
|
+
label: labelText,
|
|
176
|
+
type,
|
|
177
|
+
required: isRequired,
|
|
178
|
+
choices: choices.length > 0 ? [...new Set(choices)] : undefined,
|
|
179
|
+
allowOther: allowOther || undefined
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
console.log(`\n🎉 Introspected ${questions.length} fields successfully. Generating schema objects...\n`);
|
|
187
|
+
|
|
188
|
+
const jsonProperties: Record<string, any> = {};
|
|
189
|
+
const jsonRequired: string[] = [];
|
|
190
|
+
const mappingSchema: Record<string, any> = {};
|
|
191
|
+
|
|
192
|
+
for (const q of questions) {
|
|
193
|
+
const key = toCamelCase(q.label);
|
|
194
|
+
|
|
195
|
+
// JSON Schema building
|
|
196
|
+
if (q.type === 'file') {
|
|
197
|
+
jsonProperties[key] = {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
fileId: { type: 'string' },
|
|
201
|
+
buffer: { type: 'object' },
|
|
202
|
+
filename: { type: 'string' },
|
|
203
|
+
mimeType: { type: 'string' }
|
|
204
|
+
},
|
|
205
|
+
required: ['filename', 'mimeType']
|
|
206
|
+
};
|
|
207
|
+
} else if (q.type === 'choice') {
|
|
208
|
+
jsonProperties[key] = {
|
|
209
|
+
type: 'string'
|
|
210
|
+
};
|
|
211
|
+
if (q.choices && q.choices.length > 0) {
|
|
212
|
+
jsonProperties[key].enum = q.choices;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
jsonProperties[key] = {
|
|
216
|
+
type: 'string'
|
|
217
|
+
};
|
|
218
|
+
if (q.type === 'date') {
|
|
219
|
+
jsonProperties[key].pattern = '^\\d{4}-\\d{2}-\\d{2}$';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (q.required) {
|
|
224
|
+
jsonRequired.push(key);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Mapping Schema building
|
|
228
|
+
mappingSchema[key] = {
|
|
229
|
+
label: q.label,
|
|
230
|
+
type: q.type
|
|
231
|
+
};
|
|
232
|
+
if (q.type === 'choice') {
|
|
233
|
+
mappingSchema[key].options = {
|
|
234
|
+
choices: q.choices || []
|
|
235
|
+
};
|
|
236
|
+
if (q.allowOther) {
|
|
237
|
+
mappingSchema[key].options.allowOther = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const finalJsonSchema = {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: jsonProperties,
|
|
245
|
+
required: jsonRequired
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Print TypeScript output
|
|
249
|
+
console.log('// ==========================================');
|
|
250
|
+
console.log('// GENERATED SCHEMAS FOR GOOGLE FORM');
|
|
251
|
+
console.log('// ==========================================');
|
|
252
|
+
console.log('\nexport const JSON_SCHEMA = ' + JSON.stringify(finalJsonSchema, null, 2) + ';\n');
|
|
253
|
+
console.log('export const MAPPING_SCHEMA = ' + JSON.stringify(mappingSchema, null, 2) + ';\n');
|
|
254
|
+
|
|
255
|
+
} finally {
|
|
256
|
+
await context.close();
|
|
257
|
+
await browser.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
main().catch((err) => {
|
|
262
|
+
console.error('❌ Error during form introspection:', err.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"requestId": "1876174487",
|
|
3
|
-
"fullName": "1192130047",
|
|
4
|
-
"birthDate": "1354328376",
|
|
5
|
-
"grade": "1867801269",
|
|
6
|
-
"location": "1504377681",
|
|
7
|
-
"rate": "282114114",
|
|
8
|
-
"nds": "1347448341",
|
|
9
|
-
"exitDate": "1161802420",
|
|
10
|
-
"vacation": "1977793041",
|
|
11
|
-
"inStaff": "447126466",
|
|
12
|
-
"cvFile": "1595274093",
|
|
13
|
-
"company": "553187985",
|
|
14
|
-
"checklist": "1954006978",
|
|
15
|
-
"comment": "998553124"
|
|
16
|
-
}
|
package/CLAUDE.md
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
Default to using Bun instead of Node.js.
|
|
3
|
-
|
|
4
|
-
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
5
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
6
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
7
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
8
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
9
|
-
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
10
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
11
|
-
|
|
12
|
-
## APIs
|
|
13
|
-
|
|
14
|
-
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
15
|
-
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
16
|
-
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
17
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
18
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
19
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
20
|
-
- Bun.$`ls` instead of execa.
|
|
21
|
-
|
|
22
|
-
## Testing
|
|
23
|
-
|
|
24
|
-
Use `bun test` to run tests.
|
|
25
|
-
|
|
26
|
-
```ts#index.test.ts
|
|
27
|
-
import { test, expect } from "bun:test";
|
|
28
|
-
|
|
29
|
-
test("hello world", () => {
|
|
30
|
-
expect(1).toBe(1);
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Frontend
|
|
35
|
-
|
|
36
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
37
|
-
|
|
38
|
-
Server:
|
|
39
|
-
|
|
40
|
-
```ts#index.ts
|
|
41
|
-
import index from "./index.html"
|
|
42
|
-
|
|
43
|
-
Bun.serve({
|
|
44
|
-
routes: {
|
|
45
|
-
"/": index,
|
|
46
|
-
"/api/users/:id": {
|
|
47
|
-
GET: (req) => {
|
|
48
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
// optional websocket support
|
|
53
|
-
websocket: {
|
|
54
|
-
open: (ws) => {
|
|
55
|
-
ws.send("Hello, world!");
|
|
56
|
-
},
|
|
57
|
-
message: (ws, message) => {
|
|
58
|
-
ws.send(message);
|
|
59
|
-
},
|
|
60
|
-
close: (ws) => {
|
|
61
|
-
// handle close
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
development: {
|
|
65
|
-
hmr: true,
|
|
66
|
-
console: true,
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
72
|
-
|
|
73
|
-
```html#index.html
|
|
74
|
-
<html>
|
|
75
|
-
<body>
|
|
76
|
-
<h1>Hello, world!</h1>
|
|
77
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
78
|
-
</body>
|
|
79
|
-
</html>
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
With the following `frontend.tsx`:
|
|
83
|
-
|
|
84
|
-
```tsx#frontend.tsx
|
|
85
|
-
import React from "react";
|
|
86
|
-
import { createRoot } from "react-dom/client";
|
|
87
|
-
|
|
88
|
-
// import .css files directly and it works
|
|
89
|
-
import './index.css';
|
|
90
|
-
|
|
91
|
-
const root = createRoot(document.body);
|
|
92
|
-
|
|
93
|
-
export default function Frontend() {
|
|
94
|
-
return <h1>Hello, world!</h1>;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
root.render(<Frontend />);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Then, run index.ts
|
|
101
|
-
|
|
102
|
-
```sh
|
|
103
|
-
bun --hot ./index.ts
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|