1
0

chore: initial commit

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2025-10-14 10:40:06 -04:00
commit 716c73afc3
21 changed files with 6152 additions and 0 deletions

147
.gitignore vendored Normal file
View File

@@ -0,0 +1,147 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
.output
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
# Intellij
.idea/
# VSCode
.vscode/

12
.prettierrc.json Normal file
View File

@@ -0,0 +1,12 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"jsxSingleQuote": false,
"printWidth": 120,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
}

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/route.ts`. The page auto-updates as you edit the file.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## API Routes
This directory contains example API routes for the headless API app.
For more details, see [route.js file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route).

29
eslint.config.mts Normal file
View File

@@ -0,0 +1,29 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: {
plugins: [],
rules: {},
},
});
const eslintConfig = [
...compat.extends('next/typescript', 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
{
ignores: ['coverage/**', 'node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
// Add any API-specific rules here
},
},
];
export default eslintConfig;

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

5408
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "notion-pages-api",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"check-format": "prettier --check ."
},
"dependencies": {
"next": "15.5.4",
"zod": "^4.1.11"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0",
"@next/eslint-plugin-next": "^15.5.4",
"@types/node": "^20",
"@types/react": "^19",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.36.0",
"eslint-config-next": "^15.5.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"prettier": "^3.6.2",
"typescript": "^5"
}
}

View File

10
src/lib/schemas/index.ts Normal file
View File

@@ -0,0 +1,10 @@
export * from './notion/block.schema';
export * from './notion/colors';
export * from './notion/emoji.schema';
export * from './notion/file.schema';
export * from './notion/fileUpload.schema';
export * from './notion/page.schema';
export * from './notion/pageProperties.schema';
export * from './notion/parent.schema';
export * from './notion/richText.schema';
export * from './notion/user.schema';

View File

@@ -0,0 +1,129 @@
import { z } from 'zod';
import { emojiSchema, fileSchema, NOTION_COLORS, parentSchema, richTextSchema, userSchema } from '@/lib/schemas';
const headingsObject = z.object({
rich_text: z.array(richTextSchema),
color: z.enum(NOTION_COLORS),
is_toggleable: z.boolean(),
});
/**
* Schema representing a Notion block object.
*/
export const blockSchema = z.object({
object: z.literal('block'),
id: z.uuid(),
parent: parentSchema,
type: z.enum([
'audio',
'bookmark',
'breadcrumb',
'bulleted_list_item',
'callout',
'child_database',
'child_page',
'column',
'column_list',
'divider',
'embed',
'equation',
'file',
'heading_1',
'heading_2',
'heading_3',
'image',
'link_preview',
'numbered_list_item',
'paragraph',
'pdf',
'quote',
'synced_block',
'table',
'table_of_contents',
'table_row',
'template',
'to_do',
'toggle',
'unsupported',
'video',
]),
created_time: z.iso.datetime(),
created_by: userSchema,
last_edited_time: z.iso.datetime(),
last_edited_by: userSchema,
archived: z.boolean(),
in_trash: z.boolean(),
has_children: z.boolean(),
// Optional properties for each type
audio: fileSchema.optional(),
bookmark: z
.object({
caption: z.array(richTextSchema),
url: z.url(),
})
.optional(),
breadcrumb: z.object({}).optional(),
bulleted_list_item: z
.object({
rich_text: z.array(richTextSchema),
color: z.enum(NOTION_COLORS),
})
.optional(),
callout: z
.object({
rich_text: z.array(richTextSchema),
icon: z.union([emojiSchema, fileSchema]),
color: z.enum(NOTION_COLORS),
})
.optional(),
child_database: z
.object({
title: z.string(),
})
.optional(),
child_page: z
.object({
title: z.string(),
})
.optional(),
code: z
.object({
caption: z.array(richTextSchema),
rich_text: z.array(richTextSchema),
language: z.enum([]), // TODO: Fill in with actual languages
})
.optional(),
column_list: z.object({}).optional(),
column: z
.object({
width_ratio: z.number().min(0).max(1).optional(),
})
.optional(),
divider: z.object({}).optional(),
embed: z
.object({
url: z.url(),
})
.optional(),
equation: z.object({ expression: z.string() }).optional(),
file: z
.object({
caption: z.array(richTextSchema),
type: z.enum(['file', 'file_upload', 'external']),
file: fileSchema,
external: fileSchema,
file_upload: fileSchema,
name: z.string(),
})
.optional(),
heading_1: headingsObject.optional(),
heading_2: headingsObject.optional(),
heading_3: headingsObject.optional(),
image: fileSchema.optional(),
link_preview: z.object({ url: z.url() }).optional(),
// TODO: Continue with mention and others
});
export type NotionBlock = z.infer<typeof blockSchema>;

View File

@@ -0,0 +1,22 @@
/** Notion color options */
export const NOTION_COLORS = [
'blue',
'blue_background',
'brown',
'brown_background',
'default',
'gray',
'gray_background',
'green',
'green_background',
'orange',
'orange_background',
'pink',
'pink_background',
'purple',
'purple_background',
'red',
'red_background',
'yellow',
'yellow_background',
];

View File

@@ -0,0 +1,8 @@
import { z } from 'zod';
export const emojiSchema = z.object({
type: z.literal('emoji'),
emoji: z.string(),
});
export type NotionEmoji = z.infer<typeof emojiSchema>;

View File

@@ -0,0 +1,33 @@
import { z } from 'zod';
/**
* Schema representing a Notion file object.
*
* This schema can represent three types of files:
* 1. 'file': A file hosted by Notion, with a URL and an expiry time.
* 2. 'file_upload': A file uploaded to Notion, identified by a unique ID.
* 3. 'external': A file hosted externally, represented by a URL.
*
* Each type has its own structure, and only one type will be present in a valid object.
*/
export const fileSchema = z.object({
type: z.enum(['file', 'file_upload', 'external']),
file: z
.object({
url: z.url(),
expiry_time: z.iso.datetime(),
})
.optional(),
file_upload: z
.object({
id: z.uuid(),
})
.optional(),
external: z
.object({
url: z.url(),
})
.optional(),
});
export type NotionFile = z.infer<typeof fileSchema>;

View File

@@ -0,0 +1,22 @@
import { z } from 'zod';
/**
* Schema representing a Notion file upload object.
*
* Includes details about the file upload status, URLs, and metadata.
*/
export const fileUploadSchema = z.object({
object: z.literal('file_upload'),
id: z.uuid(),
created_time: z.iso.datetime(),
expiry_time: z.iso.datetime().nullable(),
status: z.enum(['pending', 'uploaded', 'expired', 'failed']),
filename: z.string(),
content_type: z.string().nullable(),
content_length: z.number().nullable(),
upload_url: z.string(),
complete_url: z.string(),
file_import_result: z.string(),
});
export type NotionFileUpload = z.infer<typeof fileUploadSchema>;

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
import { fileSchema, pagePropertiesSchema, parentSchema, userSchema } from '@/lib/schemas';
/**
* Schema representing a Notion page object.
*
* Includes metadata about the page, its properties, and its parent.
*/
export const pageSchema = z.object({
object: z.literal('page'),
id: z.uuid(),
created_time: z.iso.datetime(),
created_by: userSchema,
last_edited_time: z.iso.datetime(),
last_edited_by: userSchema,
archived: z.boolean(),
in_trash: z.boolean(),
icon: z.nullable(fileSchema),
cover: z.nullable(fileSchema),
properties: z.record(z.string(), pagePropertiesSchema),
parent: parentSchema,
url: z.url(),
public_url: z.url().nullable(),
});
export type NotionPage = z.infer<typeof pageSchema>;

View File

@@ -0,0 +1,92 @@
import { z } from 'zod';
import { NOTION_COLORS, richTextSchema, userSchema } from '@/lib/schemas';
/**
* Schema representing the properties of a Notion page.
*
* Includes various property types such as text, number, select, multi-select, date, people, files, and more.
*/
export const pagePropertiesSchema = z.object({
id: z.string(),
type: z.enum([
'checkbox',
'created_by',
'created_time',
'date',
'email',
'files',
'formula',
'last_edited_by',
'last_edited_time',
'multi_select',
'number',
'people',
'phone_number',
'relation',
'rich_text',
'rollup',
'select',
'status',
'title',
'url',
'unique_id',
'verification',
]),
// Optional properties for each type
checkbox: z.boolean().optional(),
created_by: userSchema.optional(),
created_time: z.iso.datetime().optional(),
date: z
.object({
start: z.iso.datetime(),
end: z.iso.datetime().optional(),
})
.optional(),
email: z.email().optional(),
files: z.array(z.any()).optional(),
formula: z.any().optional(),
last_edited_by: userSchema.optional(),
last_edited_time: z.iso.datetime().optional(),
multi_select: z
.array(
z.object({
id: z.string(),
name: z.string(),
color: z.enum(NOTION_COLORS),
})
)
.optional(),
number: z.number().optional(),
people: z.array(userSchema).optional(),
phone_number: z.string().optional(),
relation: z.array(z.object({ id: z.string() })).optional(),
rich_text: richTextSchema.optional(),
rollup: z.any().optional(),
select: z
.object({
id: z.string(),
name: z.string(),
color: z.enum(NOTION_COLORS),
})
.optional(),
status: z
.object({
id: z.string(),
name: z.string(),
color: z.enum(NOTION_COLORS),
})
.optional(),
title: richTextSchema.optional(),
url: z.url().optional(),
unique_id: z.string().optional(),
verification: z
.object({
state: z.string(),
verified_by: userSchema.nullable(),
date: z.iso.datetime(),
})
.optional(),
});
export type NotionPageProperties = z.infer<typeof pagePropertiesSchema>;

View File

@@ -0,0 +1,17 @@
import { z } from 'zod';
/**
* Schema representing the parent of a Notion page.
*
* Includes various types of parents such as database, data source, page, workspace, or block.
*/
export const parentSchema = z.object({
type: z.enum(['database_id', 'data_source_id', 'page_id', 'workspace', 'block_id']),
database_id: z.uuid().optional(),
data_source_id: z.uuid().optional(),
page_id: z.uuid().optional(),
workspace: z.boolean().optional(),
block_id: z.uuid().optional(),
});
export type NotionParent = z.infer<typeof parentSchema>;

View File

@@ -0,0 +1,68 @@
import { z } from 'zod';
import { NOTION_COLORS, userSchema } from '@/lib/schemas';
/**
* Schema representing a Notion rich text object.
*
* Includes text content, annotations, mentions, and equations.
*/
export const richTextSchema = z.array(
z.object({
type: z.literal('text'),
text: z.object({
content: z.string(),
link: z.url().nullable(),
}),
mention: z
.object({
type: z.enum(['database', 'date', 'link_preview', 'page', 'template_mention', 'user']),
database: z
.object({
id: z.uuid(),
})
.optional(),
date: z
.object({
start: z.iso.datetime(),
end: z.iso.datetime().optional(),
})
.optional(),
link_preview: z
.object({
url: z.url(),
})
.optional(),
page: z
.object({
id: z.uuid(),
})
.optional(),
template_mention: z
.object({
type: z.enum(['template_mention_date', 'template_mention_user']),
template_mention_date: z.enum(['today', 'now']).optional(),
template_mention_user: z.literal('me').optional(),
})
.optional(),
user: z.object(userSchema).optional(),
})
.optional(),
equation: z
.object({
expression: z.string(),
})
.optional(),
annotations: z.object({
bold: z.boolean(),
italic: z.boolean(),
strikethrough: z.boolean(),
underline: z.boolean(),
code: z.boolean(),
color: z.enum(NOTION_COLORS),
}),
plain_text: z.string(),
href: z.url().optional(),
})
);
export type NotionRichText = z.infer<typeof richTextSchema>;

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
/**
* Schema representing a Notion user object.
*
* Includes both person and bot user types.
*/
export const userSchema = z.object({
object: z.literal('user'),
id: z.uuid(),
type: z.enum(['person', 'bot']).optional(),
name: z.string().optional(),
avatar_url: z.url().optional(),
});
export type NotionUser = z.infer<typeof userSchema>;

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}