Add a democratic idea submission and AI synthesis platform
Implement a full-stack application with a React frontend and a Python Flask backend. The backend integrates with an AI agent to filter political ideas for democratic values and synthesize accepted ideas into a collective voice. Includes API endpoints for idea submission, retrieval, and synthesis, along with database persistence. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 31c5f770-9905-46af-a938-9d40ef3d4404 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/Xzzm5QH Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -8,3 +8,53 @@
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SubmitIdeaBody {
|
||||
/**
|
||||
* The political idea text
|
||||
* @minLength 10
|
||||
* @maxLength 1000
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* Optional pseudonym
|
||||
* @maxLength 100
|
||||
*/
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface Idea {
|
||||
id: number;
|
||||
content: string;
|
||||
author?: string | null;
|
||||
accepted: boolean;
|
||||
rejectionReason?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface IdeaResult {
|
||||
id: number;
|
||||
accepted: boolean;
|
||||
/** Reason if rejected */
|
||||
reason?: string;
|
||||
idea?: Idea;
|
||||
}
|
||||
|
||||
export interface IdeaStats {
|
||||
total: number;
|
||||
accepted: number;
|
||||
rejected: number;
|
||||
}
|
||||
|
||||
export interface Synthesis {
|
||||
/** The synthesized voice of the people */
|
||||
text: string;
|
||||
/** Number of ideas included in the synthesis */
|
||||
ideaCount: number;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,29 @@
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
MutationFunction,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type { HealthStatus } from "./api.schemas";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
HealthStatus,
|
||||
Idea,
|
||||
IdeaResult,
|
||||
IdeaStats,
|
||||
SubmitIdeaBody,
|
||||
Synthesis,
|
||||
} from "./api.schemas";
|
||||
|
||||
import { customFetch } from "../custom-fetch";
|
||||
import type { ErrorType } from "../custom-fetch";
|
||||
import type { ErrorType, BodyType } from "../custom-fetch";
|
||||
|
||||
type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
|
||||
@@ -99,3 +110,308 @@ export function useHealthCheck<
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a civic idea to be filtered and included in the synthesis
|
||||
* @summary Submit a political idea
|
||||
*/
|
||||
export const getSubmitIdeaUrl = () => {
|
||||
return `/api/ideas`;
|
||||
};
|
||||
|
||||
export const submitIdea = async (
|
||||
submitIdeaBody: SubmitIdeaBody,
|
||||
options?: RequestInit,
|
||||
): Promise<IdeaResult> => {
|
||||
return customFetch<IdeaResult>(getSubmitIdeaUrl(), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(submitIdeaBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getSubmitIdeaMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof submitIdea>>,
|
||||
TError,
|
||||
{ data: BodyType<SubmitIdeaBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof submitIdea>>,
|
||||
TError,
|
||||
{ data: BodyType<SubmitIdeaBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["submitIdea"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof submitIdea>>,
|
||||
{ data: BodyType<SubmitIdeaBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return submitIdea(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type SubmitIdeaMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof submitIdea>>
|
||||
>;
|
||||
export type SubmitIdeaMutationBody = BodyType<SubmitIdeaBody>;
|
||||
export type SubmitIdeaMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Submit a political idea
|
||||
*/
|
||||
export const useSubmitIdea = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof submitIdea>>,
|
||||
TError,
|
||||
{ data: BodyType<SubmitIdeaBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof submitIdea>>,
|
||||
TError,
|
||||
{ data: BodyType<SubmitIdeaBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getSubmitIdeaMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all ideas that passed the democratic filter
|
||||
* @summary List all accepted ideas
|
||||
*/
|
||||
export const getListIdeasUrl = () => {
|
||||
return `/api/ideas`;
|
||||
};
|
||||
|
||||
export const listIdeas = async (options?: RequestInit): Promise<Idea[]> => {
|
||||
return customFetch<Idea[]>(getListIdeasUrl(), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getListIdeasQueryKey = () => {
|
||||
return [`/api/ideas`] as const;
|
||||
};
|
||||
|
||||
export const getListIdeasQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listIdeas>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listIdeas>>, TError, TData>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListIdeasQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listIdeas>>> = ({
|
||||
signal,
|
||||
}) => listIdeas({ signal, ...requestOptions });
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listIdeas>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListIdeasQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listIdeas>>
|
||||
>;
|
||||
export type ListIdeasQueryError = ErrorType<unknown>;
|
||||
|
||||
/**
|
||||
* @summary List all accepted ideas
|
||||
*/
|
||||
|
||||
export function useListIdeas<
|
||||
TData = Awaited<ReturnType<typeof listIdeas>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listIdeas>>, TError, TData>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListIdeasQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns counts of total, accepted and rejected ideas
|
||||
* @summary Get idea statistics
|
||||
*/
|
||||
export const getGetIdeaStatsUrl = () => {
|
||||
return `/api/ideas/stats`;
|
||||
};
|
||||
|
||||
export const getIdeaStats = async (
|
||||
options?: RequestInit,
|
||||
): Promise<IdeaStats> => {
|
||||
return customFetch<IdeaStats>(getGetIdeaStatsUrl(), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetIdeaStatsQueryKey = () => {
|
||||
return [`/api/ideas/stats`] as const;
|
||||
};
|
||||
|
||||
export const getGetIdeaStatsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getIdeaStats>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIdeaStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetIdeaStatsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIdeaStats>>> = ({
|
||||
signal,
|
||||
}) => getIdeaStats({ signal, ...requestOptions });
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIdeaStats>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetIdeaStatsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getIdeaStats>>
|
||||
>;
|
||||
export type GetIdeaStatsQueryError = ErrorType<unknown>;
|
||||
|
||||
/**
|
||||
* @summary Get idea statistics
|
||||
*/
|
||||
|
||||
export function useGetIdeaStats<
|
||||
TData = Awaited<ReturnType<typeof getIdeaStats>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIdeaStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetIdeaStatsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest synthesized text of the people's voice
|
||||
* @summary Get current synthesis
|
||||
*/
|
||||
export const getGetSynthesisUrl = () => {
|
||||
return `/api/synthesis`;
|
||||
};
|
||||
|
||||
export const getSynthesis = async (
|
||||
options?: RequestInit,
|
||||
): Promise<Synthesis> => {
|
||||
return customFetch<Synthesis>(getGetSynthesisUrl(), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSynthesisQueryKey = () => {
|
||||
return [`/api/synthesis`] as const;
|
||||
};
|
||||
|
||||
export const getGetSynthesisQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSynthesis>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSynthesis>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetSynthesisQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSynthesis>>> = ({
|
||||
signal,
|
||||
}) => getSynthesis({ signal, ...requestOptions });
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSynthesis>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSynthesisQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSynthesis>>
|
||||
>;
|
||||
export type GetSynthesisQueryError = ErrorType<unknown>;
|
||||
|
||||
/**
|
||||
* @summary Get current synthesis
|
||||
*/
|
||||
|
||||
export function useGetSynthesis<
|
||||
TData = Awaited<ReturnType<typeof getSynthesis>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSynthesis>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSynthesisQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ servers:
|
||||
tags:
|
||||
- name: health
|
||||
description: Health operations
|
||||
- name: ideas
|
||||
description: Civic idea submission and management
|
||||
- name: synthesis
|
||||
description: AI synthesis of the people's voice
|
||||
paths:
|
||||
/healthz:
|
||||
get:
|
||||
@@ -24,6 +28,75 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthStatus"
|
||||
|
||||
/ideas:
|
||||
post:
|
||||
operationId: submitIdea
|
||||
tags: [ideas]
|
||||
summary: Submit a political idea
|
||||
description: Submit a civic idea to be filtered and included in the synthesis
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SubmitIdeaBody"
|
||||
responses:
|
||||
"201":
|
||||
description: Idea submitted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdeaResult"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
get:
|
||||
operationId: listIdeas
|
||||
tags: [ideas]
|
||||
summary: List all accepted ideas
|
||||
description: Returns all ideas that passed the democratic filter
|
||||
responses:
|
||||
"200":
|
||||
description: List of accepted ideas
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Idea"
|
||||
|
||||
/ideas/stats:
|
||||
get:
|
||||
operationId: getIdeaStats
|
||||
tags: [ideas]
|
||||
summary: Get idea statistics
|
||||
description: Returns counts of total, accepted and rejected ideas
|
||||
responses:
|
||||
"200":
|
||||
description: Stats
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdeaStats"
|
||||
|
||||
/synthesis:
|
||||
get:
|
||||
operationId: getSynthesis
|
||||
tags: [synthesis]
|
||||
summary: Get current synthesis
|
||||
description: Returns the latest synthesized text of the people's voice
|
||||
responses:
|
||||
"200":
|
||||
description: Current synthesis
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Synthesis"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
HealthStatus:
|
||||
@@ -34,3 +107,99 @@ components:
|
||||
required:
|
||||
- status
|
||||
|
||||
SubmitIdeaBody:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
minLength: 10
|
||||
maxLength: 1000
|
||||
description: The political idea text
|
||||
author:
|
||||
type: string
|
||||
maxLength: 100
|
||||
description: Optional pseudonym
|
||||
required:
|
||||
- content
|
||||
|
||||
IdeaResult:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
accepted:
|
||||
type: boolean
|
||||
reason:
|
||||
type: string
|
||||
description: Reason if rejected
|
||||
idea:
|
||||
$ref: "#/components/schemas/Idea"
|
||||
required:
|
||||
- id
|
||||
- accepted
|
||||
|
||||
Idea:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
content:
|
||||
type: string
|
||||
author:
|
||||
type: string
|
||||
nullable: true
|
||||
accepted:
|
||||
type: boolean
|
||||
rejectionReason:
|
||||
type: string
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- content
|
||||
- accepted
|
||||
- createdAt
|
||||
|
||||
IdeaStats:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
accepted:
|
||||
type: integer
|
||||
rejected:
|
||||
type: integer
|
||||
required:
|
||||
- total
|
||||
- accepted
|
||||
- rejected
|
||||
|
||||
Synthesis:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: The synthesized voice of the people
|
||||
ideaCount:
|
||||
type: integer
|
||||
description: Number of ideas included in the synthesis
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
required:
|
||||
- text
|
||||
- ideaCount
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- error
|
||||
- message
|
||||
|
||||
@@ -14,3 +14,59 @@ import * as zod from "zod";
|
||||
export const HealthCheckResponse = zod.object({
|
||||
status: zod.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Submit a civic idea to be filtered and included in the synthesis
|
||||
* @summary Submit a political idea
|
||||
*/
|
||||
export const submitIdeaBodyContentMin = 10;
|
||||
export const submitIdeaBodyContentMax = 1000;
|
||||
|
||||
export const submitIdeaBodyAuthorMax = 100;
|
||||
|
||||
export const SubmitIdeaBody = zod.object({
|
||||
content: zod
|
||||
.string()
|
||||
.min(submitIdeaBodyContentMin)
|
||||
.max(submitIdeaBodyContentMax)
|
||||
.describe("The political idea text"),
|
||||
author: zod
|
||||
.string()
|
||||
.max(submitIdeaBodyAuthorMax)
|
||||
.optional()
|
||||
.describe("Optional pseudonym"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns all ideas that passed the democratic filter
|
||||
* @summary List all accepted ideas
|
||||
*/
|
||||
export const ListIdeasResponseItem = zod.object({
|
||||
id: zod.number(),
|
||||
content: zod.string(),
|
||||
author: zod.string().nullish(),
|
||||
accepted: zod.boolean(),
|
||||
rejectionReason: zod.string().nullish(),
|
||||
createdAt: zod.coerce.date(),
|
||||
});
|
||||
export const ListIdeasResponse = zod.array(ListIdeasResponseItem);
|
||||
|
||||
/**
|
||||
* Returns counts of total, accepted and rejected ideas
|
||||
* @summary Get idea statistics
|
||||
*/
|
||||
export const GetIdeaStatsResponse = zod.object({
|
||||
total: zod.number(),
|
||||
accepted: zod.number(),
|
||||
rejected: zod.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the latest synthesized text of the people's voice
|
||||
* @summary Get current synthesis
|
||||
*/
|
||||
export const GetSynthesisResponse = zod.object({
|
||||
text: zod.string().describe("The synthesized voice of the people"),
|
||||
ideaCount: zod.number().describe("Number of ideas included in the synthesis"),
|
||||
updatedAt: zod.coerce.date().nullish(),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface Idea {
|
||||
id: number;
|
||||
content: string;
|
||||
author?: string | null;
|
||||
accepted: boolean;
|
||||
rejectionReason?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import type { Idea } from "./idea";
|
||||
|
||||
export interface IdeaResult {
|
||||
id: number;
|
||||
accepted: boolean;
|
||||
/** Reason if rejected */
|
||||
reason?: string;
|
||||
idea?: Idea;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface IdeaStats {
|
||||
total: number;
|
||||
accepted: number;
|
||||
rejected: number;
|
||||
}
|
||||
@@ -6,4 +6,10 @@
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export * from "./errorResponse";
|
||||
export * from "./healthStatus";
|
||||
export * from "./idea";
|
||||
export * from "./ideaResult";
|
||||
export * from "./ideaStats";
|
||||
export * from "./submitIdeaBody";
|
||||
export * from "./synthesis";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface SubmitIdeaBody {
|
||||
/**
|
||||
* The political idea text
|
||||
* @minLength 10
|
||||
* @maxLength 1000
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* Optional pseudonym
|
||||
* @maxLength 100
|
||||
*/
|
||||
author?: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface Synthesis {
|
||||
/** The synthesized voice of the people */
|
||||
text: string;
|
||||
/** Number of ideas included in the synthesis */
|
||||
ideaCount: number;
|
||||
updatedAt?: Date | null;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { pgTable, text, serial, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ideasTable = pgTable("ideas", {
|
||||
id: serial("id").primaryKey(),
|
||||
content: text("content").notNull(),
|
||||
author: text("author"),
|
||||
accepted: boolean("accepted").notNull().default(false),
|
||||
rejectionReason: text("rejection_reason"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const insertIdeaSchema = createInsertSchema(ideasTable).omit({ id: true, createdAt: true });
|
||||
export type InsertIdea = z.infer<typeof insertIdeaSchema>;
|
||||
export type Idea = typeof ideasTable.$inferSelect;
|
||||
@@ -1,20 +1,2 @@
|
||||
// Export your models here. Add one export per file
|
||||
// export * from "./posts";
|
||||
//
|
||||
// Each model/table should ideally be split into different files.
|
||||
// Each model/table should define a Drizzle table, insert schema, and types:
|
||||
//
|
||||
// import { pgTable, text, serial } from "drizzle-orm/pg-core";
|
||||
// import { createInsertSchema } from "drizzle-zod";
|
||||
// import { z } from "zod/v4";
|
||||
//
|
||||
// export const postsTable = pgTable("posts", {
|
||||
// id: serial("id").primaryKey(),
|
||||
// title: text("title").notNull(),
|
||||
// });
|
||||
//
|
||||
// export const insertPostSchema = createInsertSchema(postsTable).omit({ id: true });
|
||||
// export type InsertPost = z.infer<typeof insertPostSchema>;
|
||||
// export type Post = typeof postsTable.$inferSelect;
|
||||
|
||||
export {}
|
||||
export * from "./ideas";
|
||||
export * from "./synthesis";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { pgTable, text, serial, timestamp, integer } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const synthesisTable = pgTable("synthesis", {
|
||||
id: serial("id").primaryKey(),
|
||||
text: text("text").notNull(),
|
||||
ideaCount: integer("idea_count").notNull().default(0),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
});
|
||||
|
||||
export const insertSynthesisSchema = createInsertSchema(synthesisTable).omit({ id: true, updatedAt: true });
|
||||
export type InsertSynthesis = z.infer<typeof insertSynthesisSchema>;
|
||||
export type Synthesis = typeof synthesisTable.$inferSelect;
|
||||
Reference in New Issue
Block a user