diff --git a/pages/_app.js b/pages/_app.js new file mode 100644 index 0000000..85ee8b7 --- /dev/null +++ b/pages/_app.js @@ -0,0 +1,25 @@ +import '../app/globals.css'; +import { Providers } from '../app/providers'; +import EmotionRegistry from '../app/emotion-registry'; + +function App({ Component, pageProps, authToken }) { + return ( + + + + + + ); +} + +App.getInitialProps = async ({ ctx }) => { + // Read auth token from cookies on the server + const authToken = ctx.req?.headers.cookie + ?.split(';') + .find(c => c.trim().startsWith('spacy_auth=')) + ?.split('=')[1]; + + return { authToken }; +}; + +export default App; diff --git a/pages/api/login.js b/pages/api/login.js index fa0b992..ef97322 100644 --- a/pages/api/login.js +++ b/pages/api/login.js @@ -41,6 +41,8 @@ export default async function (req, res) { error: "Invalid login", }); } else { + // Decode Relay global ID (["public", "users", uuid]) to get the raw UUID + const userId = JSON.parse(Buffer.from(user.node.id, "base64").toString())[3]; const token = jwt.sign( { email, @@ -48,6 +50,7 @@ export default async function (req, res) { "x-hasura-allowed-roles": ["user"], "x-hasura-default-role": "user", "x-hasura-user-email": email, + "x-hasura-user-id": userId, }, }, process.env.HASURA_SECRET, diff --git a/pages/api/sign_up.js b/pages/api/sign_up.js index 1fe14c4..b1296c4 100644 --- a/pages/api/sign_up.js +++ b/pages/api/sign_up.js @@ -33,6 +33,8 @@ export default async function (req, res) { }); } + // Decode Relay global ID (["public", "users", uuid]) to get the raw UUID + const userId = JSON.parse(Buffer.from(result.data.insertUsersOne.id, "base64").toString())[3]; const token = jwt.sign( { email: req.body.input.input.email, @@ -40,6 +42,7 @@ export default async function (req, res) { "x-hasura-allowed-roles": ["user"], "x-hasura-default-role": "user", "x-hasura-user-email": input.email, + "x-hasura-user-id": userId, }, }, process.env.HASURA_SECRET, diff --git a/schema.graphql b/schema.graphql index d2ae92a..02da1b2 100644 --- a/schema.graphql +++ b/schema.graphql @@ -22,7 +22,12 @@ type Articles implements Node { createdAt: timestamptz! id: ID! intro: String! + slug: String! + title: String! updatedAt: timestamptz! + + """An object relationship""" + user: Users! } """ @@ -37,7 +42,10 @@ input ArticlesBoolExp { createdAt: TimestamptzComparisonExp id: UuidComparisonExp intro: StringComparisonExp + slug: StringComparisonExp + title: StringComparisonExp updatedAt: TimestamptzComparisonExp + user: UsersBoolExp } """ @@ -56,6 +64,11 @@ enum ArticlesConstraint { unique or primary key constraint on columns "id" """ articles_pkey + + """ + unique or primary key constraint on columns "slug" + """ + articles_slug_key } type ArticlesEdge { @@ -72,7 +85,10 @@ input ArticlesInsertInput { createdAt: timestamptz id: uuid intro: String + slug: String + title: String updatedAt: timestamptz + user: UsersObjRelInsertInput } """ @@ -102,7 +118,10 @@ input ArticlesOrderBy { createdAt: OrderBy id: OrderBy intro: OrderBy + slug: OrderBy + title: OrderBy updatedAt: OrderBy + user: UsersOrderBy } """primary key columns input for table: articles""" @@ -129,6 +148,12 @@ enum ArticlesSelectColumn { """column name""" intro + """column name""" + slug + + """column name""" + title + """column name""" updatedAt } @@ -142,6 +167,8 @@ input ArticlesSetInput { createdAt: timestamptz id: uuid intro: String + slug: String + title: String updatedAt: timestamptz } @@ -164,6 +191,12 @@ enum ArticlesUpdateColumn { """column name""" intro + """column name""" + slug + + """column name""" + title + """column name""" updatedAt } @@ -610,6 +643,16 @@ type UsersMutationResponse { returning: [Users!]! } +""" +input type for inserting object relation for remote table "users" +""" +input UsersObjRelInsertInput { + data: UsersInsertInput! + + """upsert condition""" + onConflict: UsersOnConflict +} + """ on_conflict condition type for table "users" """ diff --git a/spacy-server/metadata/databases/default/tables/public_articles.yaml b/spacy-server/metadata/databases/default/tables/public_articles.yaml index 1c43585..98ee798 100644 --- a/spacy-server/metadata/databases/default/tables/public_articles.yaml +++ b/spacy-server/metadata/databases/default/tables/public_articles.yaml @@ -1,29 +1,53 @@ table: name: articles schema: public +object_relationships: + - name: user + using: + foreign_key_constraint_on: author_id insert_permissions: - role: user permission: check: {} + set: + author_id: x-hasura-User-Id columns: + - author_id - body - - intro - created_at - - updated_at - - author_id - id + - intro + - slug + - title + - updated_at comment: "" select_permissions: - - role: user + - role: anonymous permission: columns: - body - intro + - slug + - title - created_at - updated_at - author_id - id filter: {} + limit: 0 + comment: "" + - role: user + permission: + columns: + - author_id + - body + - created_at + - id + - intro + - slug + - title + - updated_at + filter: {} comment: "" update_permissions: - role: user @@ -32,6 +56,7 @@ update_permissions: - body - created_at - intro + - title - updated_at filter: {} check: null diff --git a/src/pages/article/NewArticle.res b/src/pages/article/NewArticle.res index 01c77e8..477e225 100644 --- a/src/pages/article/NewArticle.res +++ b/src/pages/article/NewArticle.res @@ -6,6 +6,27 @@ open AncestorSpacy +module CreateArticleMutation = %relay(` +mutation NewArticleMutation($input: ArticlesInsertInput!) { + insertArticlesOne(object: $input) { + id + slug + } +} +`) + +module Query = %relay(` +query NewArticleQuery { + usersConnection(first: 1) { + edges { + node { + id + } + } + } +} +`) + module FormFields = %lenses( type state = { title: string, @@ -26,9 +47,47 @@ let formSchema = { ]) } -let default = () => { +// Helper function to create a URL-friendly slug from a title +let createSlug = title => { + title + ->Js.String2.toLowerCase + ->Js.String2.replaceByRe(%re("/[^a-z0-9]+/g"), "-") + ->Js.String2.replaceByRe(%re("/^-+|-+$/g"), "") +} + +@react.component +let make = () => { + let (mutate, _) = CreateArticleMutation.use() + let queryData = Query.use(~variables=(), ()) + let user = queryData.usersConnection.edges[0] + let handleSubmit = (event: Form.onSubmitAPI) => { - Js.log(event.state) + let slug = createSlug(event.state.values.title) + + mutate( + ~variables={ + input: { + title: Some(event.state.values.title), + intro: Some(event.state.values.short), + body: Some(event.state.values.content), + slug: Some(slug), + authorId: None, + createdAt: None, + id: None, + updatedAt: None, + user: None + }, + }, + ~onCompleted=(response, _errors) => { + switch response.insertArticlesOne { + | Some(article) => + // Navigate to the article page + Js.log2("Article created with slug:", article.slug) + | None => Js.log("Failed to create article") + } + }, + (), + )->RescriptRelay.Disposable.ignore None } @@ -84,3 +143,5 @@ let default = () => { } + +let default = make