Skip to content

Commit 4c87a98

Browse files
committed
Added CSV Importer example
1 parent 5a59ea4 commit 4c87a98

11 files changed

Lines changed: 1070 additions & 206 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FRAMER_PROJECT_URL=https://development.framer.com/projects/Sites--aabbccddeeff
2+
FRAMER_API_KEY=12345678-1234-1234-1234-123456789

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Framer Server API Examples
2+
3+
This repository contains examples for the Framer Server API. Each example is a standalone project that can be run independently.
4+
5+
## How to run examples
6+
7+
You need to obtain a Framer project URL and API key. You can get them from the Framer project settings.
8+
9+
Then, you need to set the `FRAMER_PROJECT_URL` and `FRAMER_API_KEY` environment variables.

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"linter": {
2222
"enabled": true,
2323
"rules": {
24-
"recommended": true
24+
"recommended": true,
25+
"useLiteralKeys": "off"
2526
}
2627
},
2728
"javascript": {

examples/csv-importer/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# CSV Importer
2+
3+
This example shows how to import a CSV file into a Framer collection.
4+
5+
How to use:
6+
7+
```bash
8+
npx tsx --env-file=../../.env src/csv-to-collection.ts
9+
10+
bun run src/csv-to-collection.ts
11+
12+
deno run src/csv-to-collection.ts
13+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
slug,title,description,price,inStock,category
2+
wireless-mouse,Wireless Mouse,Ergonomic wireless mouse with precision tracking,29.99,true,Electronics
3+
mechanical-keyboard,Mechanical Keyboard,RGB mechanical keyboard with cherry switches,89.99,true,Electronics
4+
usb-c-cable,USB-C Cable,Fast charging USB-C cable 6ft length,12.99,false,Accessories
5+
monitor-stand,Monitor Stand,Adjustable aluminum monitor stand,49.99,true,Furniture
6+
desk-lamp,LED Desk Lamp,Dimmable LED lamp with USB charging port,34.99,true,Lighting
7+
webcam-hd,HD Webcam,1080p webcam with built-in microphone,59.99,true,Electronics
8+
mouse-pad,Large Mouse Pad,Extended gaming mouse pad with stitched edges,19.99,true,Accessories
9+
headphone-stand,Headphone Stand,Wooden headphone stand with cable holder,24.99,false,Furniture

examples/csv-importer/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
"typecheck": "tsc --noEmit"
88
},
99
"dependencies": {
10+
"framer-api": "^0.0.1-alpha.6",
11+
"papaparse": "^5.5.3",
1012
"typescript": "^5.9.3"
13+
},
14+
"devDependencies": {
15+
"@types/node": "^22.10.2",
16+
"@types/papaparse": "^5.3.15",
17+
"tsx": "^4.21.0"
1118
}
1219
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import assert from "node:assert";
2+
import path from "node:path";
3+
import { type CreateField, connect, type FieldDataEntryInput, type FieldDataInput } from "framer-api";
4+
import { type FieldType, loadCsv } from "./load-csv";
5+
6+
// Configuration
7+
8+
const projectUrl = process.env["FRAMER_PROJECT_URL"];
9+
if (!projectUrl) {
10+
console.error("FRAMER_PROJECT_URL environment variable is required");
11+
process.exit(1);
12+
}
13+
14+
const csvPath = process.env["CSV_PATH"] ?? path.join(import.meta.dirname, "../data/sample-products.csv");
15+
const collectionName = process.env["COLLECTION_NAME"] ?? "Products";
16+
17+
const { columns, rows, fieldTypes } = loadCsv(csvPath);
18+
19+
if (!columns.includes("slug")) {
20+
console.error("CSV must contain a 'slug' column");
21+
process.exit(1);
22+
}
23+
24+
using framer = await connect(projectUrl);
25+
26+
// Find or Create Collection
27+
28+
const existingCollections = await framer.getCollections();
29+
let collection = existingCollections.find((c) => c.name === collectionName);
30+
31+
if (!collection) {
32+
collection = await framer.createCollection(collectionName);
33+
}
34+
35+
// Add Missing Fields
36+
37+
const existingFields = await collection.getFields();
38+
const existingFieldNames = new Set(existingFields.map((f) => f.name.toLowerCase()));
39+
40+
const fieldsToCreate = columns
41+
.filter((column) => column !== "slug" && !existingFieldNames.has(column.toLowerCase()))
42+
.map(
43+
(column): CreateField => ({
44+
type: fieldTypes.get(column) ?? "string",
45+
name: column,
46+
}),
47+
);
48+
49+
if (fieldsToCreate.length > 0) {
50+
await collection.addFields(fieldsToCreate);
51+
}
52+
53+
// Build Items & Import
54+
55+
const fields = await collection.getFields();
56+
const fieldNameToId = new Map(fields.map((f) => [f.name.toLowerCase(), f.id]));
57+
58+
const existingItems = await collection.getItems();
59+
const slugToExistingId = new Map(existingItems.map((item) => [item.slug, item.id]));
60+
61+
const items = rows.map((row) => {
62+
const fieldData: FieldDataInput = {};
63+
64+
for (const column of columns) {
65+
if (column === "slug") continue;
66+
67+
const fieldId = fieldNameToId.get(column.toLowerCase());
68+
if (!fieldId) continue;
69+
70+
const value = row[column] ?? "";
71+
const fieldType = fieldTypes.get(column) ?? "string";
72+
fieldData[fieldId] = toFieldData(value, fieldType);
73+
}
74+
75+
const slug = row["slug"];
76+
assert(slug && slug.length > 0, "slug is required and must be non-empty");
77+
const existingId = slugToExistingId.get(slug);
78+
79+
return { id: existingId, slug, fieldData };
80+
});
81+
82+
await collection.addItems(items);
83+
84+
console.log(`Imported ${items.length} items`);
85+
86+
function toFieldData(value: string, type: FieldType): FieldDataEntryInput {
87+
switch (type) {
88+
case "boolean":
89+
return { type: "boolean" as const, value: value.toLowerCase() === "true" };
90+
case "number":
91+
return { type: "number" as const, value: parseFloat(value) || 0 };
92+
case "string":
93+
return { type: "string" as const, value };
94+
}
95+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { readFileSync } from "node:fs";
2+
import Papa from "papaparse";
3+
4+
export type FieldType = "string" | "number" | "boolean";
5+
6+
export interface CsvData {
7+
columns: string[];
8+
rows: Record<string, string>[];
9+
fieldTypes: Map<string, FieldType>;
10+
}
11+
12+
export function loadCsv(path: string): CsvData {
13+
const csvContent = readFileSync(path, "utf-8");
14+
const { data: rows, meta } = Papa.parse<Record<string, string>>(csvContent, {
15+
header: true,
16+
skipEmptyLines: true,
17+
transformHeader: (header: string) => header.trim(),
18+
transform: (value: string) => value.trim(),
19+
});
20+
21+
if (!meta.fields) {
22+
throw new Error("CSV file has no header row");
23+
}
24+
25+
const fieldTypes = new Map(inferFieldTypes(rows, meta.fields));
26+
27+
return { columns: meta.fields, rows, fieldTypes };
28+
}
29+
30+
function inferFieldType(values: string[]): FieldType {
31+
const nonEmptyValues = values.filter((v) => v !== "");
32+
if (nonEmptyValues.length === 0) return "string";
33+
34+
const allBooleans = nonEmptyValues.every((v) => v === "true" || v === "false");
35+
if (allBooleans) return "boolean";
36+
37+
const allNumbers = nonEmptyValues.every((v) => !Number.isNaN(parseFloat(v)) && Number.isFinite(Number(v)));
38+
if (allNumbers) return "number";
39+
40+
return "string";
41+
}
42+
43+
/**
44+
* Infer the field types from the data in the CSV file.
45+
* Returns the column name and the inferred field type.
46+
*/
47+
function inferFieldTypes(rows: Record<string, string>[], columns: string[]): [string, FieldType][] {
48+
return columns.map((column) => {
49+
const values = rows.map((row) => row[column] ?? "");
50+
return [column, inferFieldType(values)];
51+
});
52+
}

examples/csv-importer/test.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)