feat: Template CRUD Endpoint (#731)

* feat: template crud endpoint

* fix: clippy error

* fix: categories for related view

* fix: add created at and last updated at to template response

* feat: template api delete endpoint

* feat: include number of template count for template creator

* fix: use params instead of individual fields for template api

* fix: seach template creator by name query

* chore: simplify query

* feat: support template count limit for template homepage
This commit is contained in:
Khor Shu Heng 2024-08-20 15:22:56 +08:00 committed by GitHub
parent 256e5ca22d
commit 9c8e718246
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2082 additions and 151 deletions

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_template_view\n WHERE view_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "09cf032adce81ba99362b3df50ba104f4e1eb2d538350c65cf615ea13f1c37f0"
}

View file

@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n tc.creator_id AS \"id!\",\n name AS \"name!\",\n avatar_url AS \"avatar_url!\",\n ARRAY_AGG((al.link_type, al.url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\"\n FROM af_template_creator tc\n LEFT OUTER JOIN af_template_creator_account_link al\n ON tc.creator_id = al.creator_id\n WHERE tc.creator_id = $1\n GROUP BY (tc.creator_id, name, avatar_url)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "avatar_url!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
null
]
},
"hash": "13e511cfa6be2924fe6b141651a49ed3c36d12aa844322b2a30fa4ce01e0b361"
}

View file

@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_template_view SET\n updated_at = NOW(),\n name = $2,\n description = $3,\n about = $4,\n view_url = $5,\n creator_id = $6,\n is_new_template = $7,\n is_featured = $8\n WHERE view_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Text",
"Text",
"Uuid",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "2394226650959b34ae80b1948b7a111720b3ea5da48934d8d7e395ecc84e6985"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH creator_number_of_templates AS (\n SELECT\n creator_id,\n COUNT(1)::int AS number_of_templates\n FROM af_template_view\n WHERE name ILIKE $1\n GROUP BY creator_id\n )\n\n SELECT\n creator.creator_id AS \"id!\",\n name AS \"name!\",\n avatar_url AS \"avatar_url!\",\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\",\n COALESCE(number_of_templates, 0) AS \"number_of_templates!\"\n FROM af_template_creator creator\n LEFT OUTER JOIN af_template_creator_account_link account_link\n USING (creator_id)\n LEFT OUTER JOIN creator_number_of_templates\n USING (creator_id)\n WHERE name ILIKE $1\n GROUP BY (creator.creator_id, name, avatar_url, number_of_templates)\n ORDER BY created_at ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "avatar_url!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
},
{
"ordinal": 4,
"name": "number_of_templates!",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
null,
null
]
},
"hash": "340b8cef5a7676541b86505cdf103fcb5b54c40a9d6e599dc1d9dc0a95e1e862"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH\n updated_creator AS (\n UPDATE af_template_creator\n SET name = $2, avatar_url = $3, updated_at = NOW()\n WHERE creator_id = $1\n RETURNING creator_id, name, avatar_url\n ),\n account_links AS (\n INSERT INTO af_template_creator_account_link (creator_id, link_type, url)\n SELECT updated_creator.creator_id as creator_id, link_type, url FROM\n UNNEST($4::text[], $5::text[]) AS t(link_type, url)\n CROSS JOIN updated_creator\n RETURNING\n creator_id,\n link_type,\n url\n )\n SELECT\n updated_creator.creator_id AS id,\n name,\n avatar_url,\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\"\n FROM updated_creator\n LEFT OUTER JOIN account_links\n ON updated_creator.creator_id = account_links.creator_id\n GROUP BY (id, name, avatar_url)\n ",
"query": "\n WITH\n updated_creator AS (\n UPDATE af_template_creator\n SET name = $2, avatar_url = $3, updated_at = NOW()\n WHERE creator_id = $1\n RETURNING creator_id, name, avatar_url\n ),\n account_links AS (\n INSERT INTO af_template_creator_account_link (creator_id, link_type, url)\n SELECT updated_creator.creator_id as creator_id, link_type, url FROM\n UNNEST($4::text[], $5::text[]) AS t(link_type, url)\n CROSS JOIN updated_creator\n RETURNING\n creator_id,\n link_type,\n url\n ),\n creator_number_of_templates AS (\n SELECT\n creator_id,\n COUNT(1)::int AS number_of_templates\n FROM af_template_view\n WHERE creator_id = $1\n GROUP BY creator_id\n )\n SELECT\n updated_creator.creator_id AS id,\n name,\n avatar_url,\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\",\n COALESCE(number_of_templates, 0) AS \"number_of_templates!\"\n FROM updated_creator\n LEFT OUTER JOIN account_links\n USING (creator_id)\n LEFT OUTER JOIN creator_number_of_templates\n USING (creator_id)\n GROUP BY (id, name, avatar_url, number_of_templates)\n ",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
},
{
"ordinal": 4,
"name": "number_of_templates!",
"type_info": "Int4"
}
],
"parameters": {
@ -37,8 +42,9 @@
false,
false,
false,
null,
null
]
},
"hash": "fcf9b5bd2d2184fc2041bb0ddfe4ee89c24c5bf7613f4e8641acf739c00ffc17"
"hash": "3ca587826f0598e7786c765dcb2fcd6ae08d8aa404f02920307547c769a3f91b"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_related_template_view\n WHERE view_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "3d3309a4ae7a88b3f7c9608dd78a1c1dc9b237a37e29722bcd2910bd23f9d873"
}

View file

@ -0,0 +1,158 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH recent_template AS (\n SELECT\n template_template_category.category_id,\n template_template_category.view_id,\n category.name,\n category.icon,\n category.bg_color,\n ROW_NUMBER() OVER (PARTITION BY template_template_category.category_id ORDER BY template.created_at DESC) AS recency\n FROM af_template_view_template_category template_template_category\n JOIN af_template_category category\n USING (category_id)\n JOIN af_template_view template\n USING (view_id)\n ),\n template_group_by_category_and_view AS (\n SELECT\n category_id,\n view_id,\n ARRAY_AGG((\n category_id,\n name,\n icon,\n bg_color\n )::template_category_minimal_type) AS categories\n FROM recent_template\n WHERE recency <= $1\n GROUP BY category_id, view_id\n ),\n template_group_by_category_and_view_with_creator_and_template_details AS (\n SELECT\n template_group_by_category_and_view.category_id,\n (\n template.view_id,\n template.created_at,\n template.updated_at,\n template.name,\n template.description,\n template.view_url,\n (\n creator.creator_id,\n creator.name,\n creator.avatar_url\n )::template_creator_minimal_type,\n template_group_by_category_and_view.categories,\n template.is_new_template,\n template.is_featured\n )::template_minimal_type AS template\n FROM template_group_by_category_and_view\n JOIN af_template_view template\n USING (view_id)\n JOIN af_template_creator creator\n USING (creator_id)\n ),\n template_group_by_category AS (\n SELECT\n category_id,\n ARRAY_AGG(template) AS templates\n FROM template_group_by_category_and_view_with_creator_and_template_details\n GROUP BY category_id\n )\n SELECT\n (\n template_group_by_category.category_id,\n category.name,\n category.icon,\n category.bg_color\n )::template_category_minimal_type AS \"category!: AFTemplateCategoryMinimalRow\",\n templates AS \"templates!: Vec<AFTemplateMinimalRow>\"\n FROM template_group_by_category\n JOIN af_template_category category\n USING (category_id)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "category!: AFTemplateCategoryMinimalRow",
"type_info": {
"Custom": {
"name": "template_category_minimal_type",
"kind": {
"Composite": [
[
"category_id",
"Uuid"
],
[
"name",
"Text"
],
[
"icon",
"Text"
],
[
"bg_color",
"Text"
]
]
}
}
}
},
{
"ordinal": 1,
"name": "templates!: Vec<AFTemplateMinimalRow>",
"type_info": {
"Custom": {
"name": "template_minimal_type[]",
"kind": {
"Array": {
"Custom": {
"name": "template_minimal_type",
"kind": {
"Composite": [
[
"view_id",
"Uuid"
],
[
"created_at",
"Timestamptz"
],
[
"updated_at",
"Timestamptz"
],
[
"name",
"Text"
],
[
"description",
"Text"
],
[
"view_url",
"Text"
],
[
"creator",
{
"Custom": {
"name": "template_creator_minimal_type",
"kind": {
"Composite": [
[
"creator_id",
"Uuid"
],
[
"name",
"Text"
],
[
"avatar_url",
"Text"
]
]
}
}
}
],
[
"categories",
{
"Custom": {
"name": "template_category_minimal_type[]",
"kind": {
"Array": {
"Custom": {
"name": "template_category_minimal_type",
"kind": {
"Composite": [
[
"category_id",
"Uuid"
],
[
"name",
"Text"
],
[
"icon",
"Text"
],
[
"bg_color",
"Text"
]
]
}
}
}
}
}
}
],
[
"is_new_template",
"Bool"
],
[
"is_featured",
"Bool"
]
]
}
}
}
}
}
}
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null,
null
]
},
"hash": "50052ba4fa38e18bcf7d6ef76f8ffdb0263dfc0eb6aa001a8c30ab881ce3300e"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH\n new_creator AS (\n INSERT INTO af_template_creator (name, avatar_url)\n VALUES ($1, $2)\n RETURNING creator_id, name, avatar_url\n ),\n account_links AS (\n INSERT INTO af_template_creator_account_link (creator_id, link_type, url)\n SELECT new_creator.creator_id as creator_id, link_type, url FROM\n UNNEST($3::text[], $4::text[]) AS t(link_type, url)\n CROSS JOIN new_creator\n RETURNING\n creator_id,\n link_type,\n url\n )\n SELECT\n new_creator.creator_id AS id,\n name,\n avatar_url,\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\"\n FROM new_creator\n LEFT OUTER JOIN account_links\n ON new_creator.creator_id = account_links.creator_id\n GROUP BY (id, name, avatar_url)\n ",
"query": "\n WITH\n new_creator AS (\n INSERT INTO af_template_creator (name, avatar_url)\n VALUES ($1, $2)\n RETURNING creator_id, name, avatar_url\n ),\n account_links AS (\n INSERT INTO af_template_creator_account_link (creator_id, link_type, url)\n SELECT new_creator.creator_id as creator_id, link_type, url FROM\n UNNEST($3::text[], $4::text[]) AS t(link_type, url)\n CROSS JOIN new_creator\n RETURNING\n creator_id,\n link_type,\n url\n )\n SELECT\n new_creator.creator_id AS id,\n name,\n avatar_url,\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\",\n 0 AS \"number_of_templates!\"\n FROM new_creator\n LEFT OUTER JOIN account_links\n USING (creator_id)\n GROUP BY (id, name, avatar_url)\n ",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
},
{
"ordinal": 4,
"name": "number_of_templates!",
"type_info": "Int4"
}
],
"parameters": {
@ -36,8 +41,9 @@
false,
false,
false,
null,
null
]
},
"hash": "23ce30adcad72a7beba4db6a1a9a5947b433aa28d1ac8a1e9fa328aa751fe4a2"
"hash": "523087b0101a35abfc70a561272acec7a357491a86901f7927b8242173b5c8c8"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_template_view_template_category\n WHERE view_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "84e600f13d61c56a45133e7458d5152e68dec72030e5789bf4149a333b6ebdf5"
}

View file

@ -0,0 +1,245 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH template_with_creator_account_link AS (\n SELECT\n template.view_id,\n template.creator_id,\n COALESCE(\n ARRAY_AGG((link_type, url)::account_link_type) FILTER (WHERE link_type IS NOT NULL),\n '{}'\n ) AS account_links\n FROM af_template_view template\n JOIN af_template_creator creator\n USING (creator_id)\n LEFT OUTER JOIN af_template_creator_account_link account_link\n USING (creator_id)\n WHERE view_id = $1\n GROUP BY (view_id, template.creator_id)\n ),\n related_template_with_category AS (\n SELECT\n template.related_view_id,\n ARRAY_AGG(\n (\n template_category.category_id,\n template_category.name,\n template_category.icon,\n template_category.bg_color\n )::template_category_minimal_type\n ) AS categories\n FROM af_related_template_view template\n JOIN af_template_view_template_category template_template_category\n ON template.related_view_id = template_template_category.view_id\n JOIN af_template_category template_category\n USING (category_id)\n WHERE template.view_id = $1\n GROUP BY template.related_view_id\n ),\n template_with_related_template AS (\n SELECT\n template.view_id,\n ARRAY_AGG(\n (\n template.related_view_id,\n related_template.created_at,\n related_template.updated_at,\n related_template.name,\n related_template.description,\n related_template.view_url,\n (\n creator.creator_id,\n creator.name,\n creator.avatar_url\n )::template_creator_minimal_type,\n related_template_with_category.categories,\n related_template.is_new_template,\n related_template.is_featured\n )::template_minimal_type\n ) AS related_templates\n FROM af_related_template_view template\n JOIN af_template_view related_template\n ON template.related_view_id = related_template.view_id\n JOIN af_template_creator creator\n ON related_template.creator_id = creator.creator_id\n JOIN related_template_with_category\n ON template.related_view_id = related_template_with_category.related_view_id\n WHERE template.view_id = $1\n GROUP BY template.view_id\n ),\n template_with_category AS (\n SELECT\n view_id,\n COALESCE(\n ARRAY_AGG((\n vtc.category_id,\n name,\n icon,\n bg_color,\n description,\n category_type,\n priority\n )) FILTER (WHERE vtc.category_id IS NOT NULL),\n '{}'\n ) AS categories\n FROM af_template_view_template_category vtc\n JOIN af_template_category tc\n ON vtc.category_id = tc.category_id\n WHERE view_id = $1\n GROUP BY view_id\n ),\n creator_number_of_templates AS (\n SELECT\n creator_id,\n COUNT(*) AS number_of_templates\n FROM af_template_view\n GROUP BY creator_id\n )\n\n SELECT\n template.view_id,\n template.created_at,\n template.updated_at,\n template.name,\n template.description,\n template.about,\n template.view_url,\n (\n creator.creator_id,\n creator.name,\n creator.avatar_url,\n template_with_creator_account_link.account_links,\n creator_number_of_templates.number_of_templates\n )::template_creator_type AS \"creator!: AFTemplateCreatorRow\",\n template_with_category.categories AS \"categories!: Vec<AFTemplateCategoryRow>\",\n COALESCE(template_with_related_template.related_templates, '{}') AS \"related_templates!: Vec<AFTemplateMinimalRow>\",\n template.is_new_template,\n template.is_featured\n FROM af_template_view template\n JOIN af_template_creator creator\n USING (creator_id)\n JOIN template_with_creator_account_link\n ON template.view_id = template_with_creator_account_link.view_id\n LEFT OUTER JOIN template_with_related_template\n ON template.view_id = template_with_related_template.view_id\n JOIN template_with_category\n ON template.view_id = template_with_category.view_id\n LEFT OUTER JOIN creator_number_of_templates\n ON template.creator_id = creator_number_of_templates.creator_id\n WHERE template.view_id = $1\n\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "view_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "about",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "view_url",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "creator!: AFTemplateCreatorRow",
"type_info": {
"Custom": {
"name": "template_creator_type",
"kind": {
"Composite": [
[
"creator_id",
"Uuid"
],
[
"name",
"Text"
],
[
"avatar_url",
"Text"
],
[
"account_links",
{
"Custom": {
"name": "account_link_type[]",
"kind": {
"Array": {
"Custom": {
"name": "account_link_type",
"kind": {
"Composite": [
[
"link_type",
"Text"
],
[
"url",
"Text"
]
]
}
}
}
}
}
}
],
[
"number_of_templates",
"Int4"
]
]
}
}
}
},
{
"ordinal": 8,
"name": "categories!: Vec<AFTemplateCategoryRow>",
"type_info": "RecordArray"
},
{
"ordinal": 9,
"name": "related_templates!: Vec<AFTemplateMinimalRow>",
"type_info": {
"Custom": {
"name": "template_minimal_type[]",
"kind": {
"Array": {
"Custom": {
"name": "template_minimal_type",
"kind": {
"Composite": [
[
"view_id",
"Uuid"
],
[
"created_at",
"Timestamptz"
],
[
"updated_at",
"Timestamptz"
],
[
"name",
"Text"
],
[
"description",
"Text"
],
[
"view_url",
"Text"
],
[
"creator",
{
"Custom": {
"name": "template_creator_minimal_type",
"kind": {
"Composite": [
[
"creator_id",
"Uuid"
],
[
"name",
"Text"
],
[
"avatar_url",
"Text"
]
]
}
}
}
],
[
"categories",
{
"Custom": {
"name": "template_category_minimal_type[]",
"kind": {
"Array": {
"Custom": {
"name": "template_category_minimal_type",
"kind": {
"Composite": [
[
"category_id",
"Uuid"
],
[
"name",
"Text"
],
[
"icon",
"Text"
],
[
"bg_color",
"Text"
]
]
}
}
}
}
}
}
],
[
"is_new_template",
"Bool"
],
[
"is_featured",
"Bool"
]
]
}
}
}
}
}
}
},
{
"ordinal": 10,
"name": "is_new_template",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "is_featured",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
null,
null,
null,
false,
false
]
},
"hash": "9b4d78e8e2c2a8d77d99f400ad1ad3b9eb936988f6cba82146f0fb91e06899d2"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_related_template_view (view_id, related_view_id)\n SELECT $1 AS view_id, related_view_id\n FROM UNNEST($2::uuid[]) AS t(related_view_id)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"UuidArray"
]
},
"nullable": []
},
"hash": "b509712055858af398fd12ddd1a8c3da54280cf55f0c53f340bddbf4bf09b3e0"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH creator_number_of_templates AS (\n SELECT\n creator_id,\n COUNT(1)::int AS number_of_templates\n FROM af_template_view\n WHERE creator_id = $1\n GROUP BY creator_id\n )\n SELECT\n creator.creator_id AS \"id!\",\n name AS \"name!\",\n avatar_url AS \"avatar_url!\",\n ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\",\n COALESCE(number_of_templates, 0) AS \"number_of_templates!\"\n FROM af_template_creator creator\n LEFT OUTER JOIN af_template_creator_account_link account_link\n USING (creator_id)\n LEFT OUTER JOIN creator_number_of_templates\n USING (creator_id)\n WHERE creator.creator_id = $1\n GROUP BY (creator.creator_id, name, avatar_url, number_of_templates)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "avatar_url!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
},
{
"ordinal": 4,
"name": "number_of_templates!",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
null,
null
]
},
"hash": "bd34e351ea1adc0d12d4f1cce5a855089b7f39a431dea2903c3e0b9a220640b8"
}

View file

@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_template_view (\n view_id,\n name,\n description,\n about,\n view_url,\n creator_id,\n is_new_template,\n is_featured\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Text",
"Text",
"Uuid",
"Bool",
"Bool"
]
},
"nullable": []
},
"hash": "ca2a21db67716e3f12b9f9240c1dba1b7cbe0bec1f59ef132fed53942ebad317"
}

View file

@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n tc.creator_id AS \"id!\",\n name AS \"name!\",\n avatar_url AS \"avatar_url!\",\n ARRAY_AGG((al.link_type, al.url)) FILTER (WHERE link_type IS NOT NULL) AS \"account_links: Vec<AccountLinkColumn>\"\n FROM af_template_creator tc\n LEFT OUTER JOIN af_template_creator_account_link al\n ON tc.creator_id = al.creator_id\n WHERE name LIKE $1\n GROUP BY (tc.creator_id, name, avatar_url)\n ORDER BY created_at ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "avatar_url!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "account_links: Vec<AccountLinkColumn>",
"type_info": "RecordArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
null
]
},
"hash": "e2e0800097f488070f216ec48be4ee9feb649dcb3ff27a0ec337e3f623c09ad0"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_template_view_template_category (view_id, category_id)\n SELECT $1 as view_id, category_id FROM\n UNNEST($2::uuid[]) AS category_id\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"UuidArray"
]
},
"nullable": []
},
"hash": "f54ced785b4fdd22c9236b566996d5d9d4a8c91902e4029fe8f8f30f3af39b39"
}

View file

@ -1,8 +1,9 @@
use client_api_entity::{
AccountLink, CreateTemplateCategoryParams, CreateTemplateCreatorParams,
GetTemplateCategoriesQueryParams, GetTemplateCreatorsQueryParams, TemplateCategories,
TemplateCategory, TemplateCategoryType, TemplateCreator, TemplateCreators,
UpdateTemplateCategoryParams, UpdateTemplateCreatorParams,
AccountLink, CreateTemplateCategoryParams, CreateTemplateCreatorParams, CreateTemplateParams,
GetTemplateCategoriesQueryParams, GetTemplateCreatorsQueryParams, GetTemplatesQueryParams,
Template, TemplateCategories, TemplateCategory, TemplateCategoryType, TemplateCreator,
TemplateCreators, Templates, UpdateTemplateCategoryParams, UpdateTemplateCreatorParams,
UpdateTemplateParams,
};
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
@ -34,6 +35,14 @@ fn template_creator_resource_url(base_url: &str, creator_id: Uuid) -> String {
)
}
fn template_resources_url(base_url: &str) -> String {
format!("{}/template", template_api_prefix(base_url))
}
fn template_resource_url(base_url: &str, view_id: Uuid) -> String {
format!("{}/{}", template_resources_url(base_url), view_id)
}
impl Client {
pub async fn create_template_category(
&self,
@ -204,4 +213,88 @@ impl Client {
.await?
.into_data()
}
pub async fn create_template(
&self,
params: &CreateTemplateParams,
) -> Result<Template, AppResponseError> {
let url = template_resources_url(&self.base_url);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(params)
.send()
.await?;
AppResponse::<Template>::from_response(resp)
.await?
.into_data()
}
pub async fn get_template(&self, view_id: Uuid) -> Result<Template, AppResponseError> {
let url = template_resource_url(&self.base_url, view_id);
let resp = self
.http_client_without_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<Template>::from_response(resp)
.await?
.into_data()
}
pub async fn get_templates(
&self,
category_id: Option<Uuid>,
is_featured: Option<bool>,
is_new_template: Option<bool>,
name_contains: Option<String>,
) -> Result<Templates, AppResponseError> {
let url = template_resources_url(&self.base_url);
let resp = self
.http_client_without_auth(Method::GET, &url)
.await?
.query(&GetTemplatesQueryParams {
category_id,
is_featured,
is_new_template,
name_contains,
})
.send()
.await?;
AppResponse::<Templates>::from_response(resp)
.await?
.into_data()
}
pub async fn update_template(
&self,
view_id: Uuid,
params: &UpdateTemplateParams,
) -> Result<Template, AppResponseError> {
let url = template_resource_url(&self.base_url, view_id);
let resp = self
.http_client_with_auth(Method::PUT, &url)
.await?
.json(params)
.send()
.await?;
AppResponse::<Template>::from_response(resp)
.await?
.into_data()
}
pub async fn delete_template(&self, view_id: Uuid) -> Result<(), AppResponseError> {
let url = template_resource_url(&self.base_url, view_id);
let resp = self
.http_client_with_auth(Method::DELETE, &url)
.await?
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
}

View file

@ -1055,6 +1055,14 @@ pub struct TemplateCategory {
pub priority: i32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateCategoryMinimal {
pub id: Uuid,
pub name: String,
pub icon: String,
pub bg_color: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateTemplateCategoryParams {
pub name: String,
@ -1098,6 +1106,14 @@ pub struct TemplateCreator {
pub name: String,
pub avatar_url: String,
pub account_links: Vec<AccountLink>,
pub number_of_templates: i32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateCreatorMinimal {
pub id: Uuid,
pub name: String,
pub avatar_url: String,
}
#[derive(Serialize, Deserialize, Debug)]
@ -1119,6 +1135,94 @@ pub struct GetTemplateCreatorsQueryParams {
pub name_contains: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Template {
pub view_id: Uuid,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
pub name: String,
pub description: String,
pub about: String,
pub view_url: String,
pub categories: Vec<TemplateCategory>,
pub creator: TemplateCreator,
pub is_new_template: bool,
pub is_featured: bool,
pub related_templates: Vec<TemplateMinimal>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateMinimal {
pub view_id: Uuid,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
pub name: String,
pub description: String,
pub view_url: String,
pub creator: TemplateCreatorMinimal,
pub categories: Vec<TemplateCategoryMinimal>,
pub is_new_template: bool,
pub is_featured: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Templates {
pub templates: Vec<TemplateMinimal>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateTemplateParams {
pub view_id: Uuid,
pub name: String,
pub description: String,
pub about: String,
pub view_url: String,
pub category_ids: Vec<Uuid>,
pub creator_id: Uuid,
pub is_new_template: bool,
pub is_featured: bool,
pub related_view_ids: Vec<Uuid>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateTemplateParams {
pub name: String,
pub description: String,
pub about: String,
pub view_url: String,
pub category_ids: Vec<Uuid>,
pub creator_id: Uuid,
pub is_new_template: bool,
pub is_featured: bool,
pub related_view_ids: Vec<Uuid>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetTemplatesQueryParams {
pub category_id: Option<Uuid>,
pub is_featured: Option<bool>,
pub is_new_template: Option<bool>,
pub name_contains: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateGroup {
pub category: TemplateCategoryMinimal,
pub templates: Vec<TemplateMinimal>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateHomePage {
pub featured_templates: Vec<TemplateMinimal>,
pub new_templates: Vec<TemplateMinimal>,
pub template_groups: Vec<TemplateGroup>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateHomePageQueryParams {
pub per_count: Option<i64>,
}
#[cfg(test)]
mod test {
use crate::dto::{CollabParams, CollabParamsV0};

View file

@ -4,7 +4,8 @@ use chrono::{DateTime, Utc};
use database_entity::dto::{
AFAccessLevel, AFRole, AFUserProfile, AFWebUser, AFWorkspace, AFWorkspaceInvitationStatus,
AccountLink, GlobalComment, Reaction, TemplateCategory, TemplateCategoryType, TemplateCreator,
AccountLink, GlobalComment, Reaction, Template, TemplateCategory, TemplateCategoryMinimal,
TemplateCategoryType, TemplateCreator, TemplateCreatorMinimal, TemplateGroup, TemplateMinimal,
};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
@ -276,9 +277,9 @@ impl From<AFReactionRow> for Reaction {
}
}
#[derive(Debug, FromRow, Serialize)]
#[derive(Debug, FromRow, Serialize, sqlx::Type)]
pub struct AFTemplateCategoryRow {
pub id: Uuid,
pub category_id: Uuid,
pub name: String,
pub icon: String,
pub bg_color: String,
@ -290,7 +291,7 @@ pub struct AFTemplateCategoryRow {
impl From<AFTemplateCategoryRow> for TemplateCategory {
fn from(value: AFTemplateCategoryRow) -> Self {
Self {
id: value.id,
id: value.category_id,
name: value.name,
icon: value.icon,
bg_color: value.bg_color,
@ -301,6 +302,26 @@ impl From<AFTemplateCategoryRow> for TemplateCategory {
}
}
#[derive(Debug, FromRow, Serialize, sqlx::Type)]
#[sqlx(type_name = "template_category_minimal_type")]
pub struct AFTemplateCategoryMinimalRow {
pub category_id: Uuid,
pub name: String,
pub icon: String,
pub bg_color: String,
}
impl From<AFTemplateCategoryMinimalRow> for TemplateCategoryMinimal {
fn from(value: AFTemplateCategoryMinimalRow) -> Self {
Self {
id: value.category_id,
name: value.name,
icon: value.icon,
bg_color: value.bg_color,
}
}
}
#[derive(sqlx::Type, Serialize, Debug)]
#[repr(i32)]
pub enum AFTemplateCategoryTypeColumn {
@ -327,6 +348,7 @@ impl From<TemplateCategoryType> for AFTemplateCategoryTypeColumn {
}
#[derive(sqlx::Type, Serialize, Debug)]
#[sqlx(type_name = "account_link_type")]
pub struct AccountLinkColumn {
pub link_type: String,
pub url: String,
@ -341,12 +363,14 @@ impl From<AccountLinkColumn> for AccountLink {
}
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, sqlx::Type)]
#[sqlx(type_name = "template_creator_type")]
pub struct AFTemplateCreatorRow {
pub id: Uuid,
pub name: String,
pub avatar_url: String,
pub account_links: Option<Vec<AccountLinkColumn>>,
pub number_of_templates: i32,
}
impl From<AFTemplateCreatorRow> for TemplateCreator {
@ -362,6 +386,119 @@ impl From<AFTemplateCreatorRow> for TemplateCreator {
name: value.name,
avatar_url: value.avatar_url,
account_links,
number_of_templates: value.number_of_templates,
}
}
}
#[derive(Debug, Serialize, sqlx::Type)]
#[sqlx(type_name = "template_creator_minimal_type")]
pub struct AFTemplateCreatorMinimalColumn {
pub creator_id: Uuid,
pub name: String,
pub avatar_url: String,
}
impl From<AFTemplateCreatorMinimalColumn> for TemplateCreatorMinimal {
fn from(value: AFTemplateCreatorMinimalColumn) -> Self {
Self {
id: value.creator_id,
name: value.name,
avatar_url: value.avatar_url,
}
}
}
#[derive(Debug, Serialize, FromRow, sqlx::Type)]
#[sqlx(type_name = "template_minimal_type")]
pub struct AFTemplateMinimalRow {
pub view_id: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub name: String,
pub description: String,
pub view_url: String,
pub creator: AFTemplateCreatorMinimalColumn,
pub categories: Vec<AFTemplateCategoryMinimalRow>,
pub is_new_template: bool,
pub is_featured: bool,
}
impl From<AFTemplateMinimalRow> for TemplateMinimal {
fn from(value: AFTemplateMinimalRow) -> Self {
Self {
view_id: value.view_id,
created_at: value.created_at,
last_updated_at: value.updated_at,
name: value.name,
description: value.description,
creator: value.creator.into(),
categories: value.categories.into_iter().map(|x| x.into()).collect(),
view_url: value.view_url,
is_new_template: value.is_new_template,
is_featured: value.is_featured,
}
}
}
#[derive(Debug, Serialize, sqlx::Type)]
pub struct AFTemplateRow {
pub view_id: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub name: String,
pub description: String,
pub about: String,
pub view_url: String,
pub creator: AFTemplateCreatorRow,
pub categories: Vec<AFTemplateCategoryRow>,
pub related_templates: Vec<AFTemplateMinimalRow>,
pub is_new_template: bool,
pub is_featured: bool,
}
impl From<AFTemplateRow> for Template {
fn from(value: AFTemplateRow) -> Self {
let mut related_templates: Vec<TemplateMinimal> = value
.related_templates
.into_iter()
.map(|v| v.into())
.collect();
related_templates.sort_by_key(|t| t.created_at);
related_templates.reverse();
Self {
view_id: value.view_id,
created_at: value.created_at,
last_updated_at: value.updated_at,
name: value.name,
description: value.description,
about: value.about,
view_url: value.view_url,
creator: value.creator.into(),
categories: value.categories.into_iter().map(|v| v.into()).collect(),
related_templates,
is_new_template: value.is_new_template,
is_featured: value.is_featured,
}
}
}
#[derive(Debug, Serialize, sqlx::Type)]
pub struct AFTemplateGroupRow {
pub category: AFTemplateCategoryMinimalRow,
pub templates: Vec<AFTemplateMinimalRow>,
}
impl From<AFTemplateGroupRow> for TemplateGroup {
fn from(value: AFTemplateGroupRow) -> Self {
let mut templates: Vec<TemplateMinimal> =
value.templates.into_iter().map(|v| v.into()).collect();
templates.sort_by_key(|t| t.created_at);
templates.reverse();
Self {
category: value.category.into(),
templates,
}
}
}

View file

@ -1,10 +1,14 @@
use app_error::AppError;
use database_entity::dto::{AccountLink, TemplateCategory, TemplateCategoryType, TemplateCreator};
use database_entity::dto::{
AccountLink, Template, TemplateCategory, TemplateCategoryType, TemplateCreator, TemplateGroup,
TemplateMinimal,
};
use sqlx::{Executor, Postgres, QueryBuilder};
use uuid::Uuid;
use crate::pg_row::{
AFTemplateCategoryRow, AFTemplateCategoryTypeColumn, AFTemplateCreatorRow, AccountLinkColumn,
AFTemplateCategoryMinimalRow, AFTemplateCategoryRow, AFTemplateCategoryTypeColumn,
AFTemplateCreatorRow, AFTemplateGroupRow, AFTemplateMinimalRow, AFTemplateRow, AccountLinkColumn,
};
pub async fn insert_new_template_category<'a, E: Executor<'a, Database = Postgres>>(
@ -98,7 +102,7 @@ pub async fn select_template_categories<'a, E: Executor<'a, Database = Postgres>
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
r#"
SELECT
category_id AS id,
category_id,
name,
description,
icon,
@ -157,7 +161,7 @@ pub async fn delete_template_category_by_id<'a, E: Executor<'a, Database = Postg
executor: E,
category_id: Uuid,
) -> Result<(), AppError> {
let rows_affected = sqlx::query!(
sqlx::query!(
r#"
DELETE FROM af_template_category
WHERE category_id = $1
@ -165,14 +169,7 @@ pub async fn delete_template_category_by_id<'a, E: Executor<'a, Database = Postg
category_id,
)
.execute(executor)
.await?
.rows_affected();
if rows_affected == 0 {
tracing::error!(
"No template category with id {} was found to delete",
category_id
);
}
.await?;
Ok(())
}
@ -210,10 +207,11 @@ pub async fn insert_template_creator<'a, E: Executor<'a, Database = Postgres>>(
new_creator.creator_id AS id,
name,
avatar_url,
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>"
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>",
0 AS "number_of_templates!"
FROM new_creator
LEFT OUTER JOIN account_links
ON new_creator.creator_id = account_links.creator_id
USING (creator_id)
GROUP BY (id, name, avatar_url)
"#,
name,
@ -258,16 +256,27 @@ pub async fn update_template_creator_by_id<'a, E: Executor<'a, Database = Postgr
creator_id,
link_type,
url
),
creator_number_of_templates AS (
SELECT
creator_id,
COUNT(1)::int AS number_of_templates
FROM af_template_view
WHERE creator_id = $1
GROUP BY creator_id
)
SELECT
updated_creator.creator_id AS id,
name,
avatar_url,
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>"
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>",
COALESCE(number_of_templates, 0) AS "number_of_templates!"
FROM updated_creator
LEFT OUTER JOIN account_links
ON updated_creator.creator_id = account_links.creator_id
GROUP BY (id, name, avatar_url)
USING (creator_id)
LEFT OUTER JOIN creator_number_of_templates
USING (creator_id)
GROUP BY (id, name, avatar_url, number_of_templates)
"#,
creator_id,
name,
@ -304,19 +313,31 @@ pub async fn select_template_creators_by_name<'a, E: Executor<'a, Database = Pos
let creator_rows = sqlx::query_as!(
AFTemplateCreatorRow,
r#"
WITH creator_number_of_templates AS (
SELECT
creator_id,
COUNT(1)::int AS number_of_templates
FROM af_template_view
WHERE name ILIKE $1
GROUP BY creator_id
)
SELECT
tc.creator_id AS "id!",
creator.creator_id AS "id!",
name AS "name!",
avatar_url AS "avatar_url!",
ARRAY_AGG((al.link_type, al.url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>"
FROM af_template_creator tc
LEFT OUTER JOIN af_template_creator_account_link al
ON tc.creator_id = al.creator_id
WHERE name LIKE $1
GROUP BY (tc.creator_id, name, avatar_url)
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>",
COALESCE(number_of_templates, 0) AS "number_of_templates!"
FROM af_template_creator creator
LEFT OUTER JOIN af_template_creator_account_link account_link
USING (creator_id)
LEFT OUTER JOIN creator_number_of_templates
USING (creator_id)
WHERE name ILIKE $1
GROUP BY (creator.creator_id, name, avatar_url, number_of_templates)
ORDER BY created_at ASC
"#,
substr_match
format!("%{}%", substr_match)
)
.fetch_all(executor)
.await?;
@ -331,16 +352,27 @@ pub async fn select_template_creator_by_id<'a, E: Executor<'a, Database = Postgr
let creator_row = sqlx::query_as!(
AFTemplateCreatorRow,
r#"
WITH creator_number_of_templates AS (
SELECT
creator_id,
COUNT(1)::int AS number_of_templates
FROM af_template_view
WHERE creator_id = $1
GROUP BY creator_id
)
SELECT
tc.creator_id AS "id!",
creator.creator_id AS "id!",
name AS "name!",
avatar_url AS "avatar_url!",
ARRAY_AGG((al.link_type, al.url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>"
FROM af_template_creator tc
LEFT OUTER JOIN af_template_creator_account_link al
ON tc.creator_id = al.creator_id
WHERE tc.creator_id = $1
GROUP BY (tc.creator_id, name, avatar_url)
ARRAY_AGG((link_type, url)) FILTER (WHERE link_type IS NOT NULL) AS "account_links: Vec<AccountLinkColumn>",
COALESCE(number_of_templates, 0) AS "number_of_templates!"
FROM af_template_creator creator
LEFT OUTER JOIN af_template_creator_account_link account_link
USING (creator_id)
LEFT OUTER JOIN creator_number_of_templates
USING (creator_id)
WHERE creator.creator_id = $1
GROUP BY (creator.creator_id, name, avatar_url, number_of_templates)
"#,
creator_id
)
@ -354,7 +386,7 @@ pub async fn delete_template_creator_by_id<'a, E: Executor<'a, Database = Postgr
executor: E,
creator_id: Uuid,
) -> Result<(), AppError> {
let rows_affected = sqlx::query!(
sqlx::query!(
r#"
DELETE FROM af_template_creator
WHERE creator_id = $1
@ -362,13 +394,493 @@ pub async fn delete_template_creator_by_id<'a, E: Executor<'a, Database = Postgr
creator_id,
)
.execute(executor)
.await?;
Ok(())
}
pub async fn insert_template_view_template_category<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
category_ids: &[Uuid],
) -> Result<(), AppError> {
let rows_affected = sqlx::query!(
r#"
INSERT INTO af_template_view_template_category (view_id, category_id)
SELECT $1 as view_id, category_id FROM
UNNEST($2::uuid[]) AS category_id
"#,
view_id,
category_ids
)
.execute(executor)
.await?
.rows_affected();
if rows_affected == 0 {
tracing::error!(
"No template creator with id {} was found to delete",
creator_id
"at least one category id is expected to be inserted for view_id {}",
view_id
);
}
Ok(())
}
pub async fn delete_template_view_template_categories<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
) -> Result<(), AppError> {
sqlx::query!(
r#"
DELETE FROM af_template_view_template_category
WHERE view_id = $1
"#,
view_id,
)
.execute(executor)
.await?;
Ok(())
}
pub async fn insert_related_templates<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
category_ids: &[Uuid],
) -> Result<(), AppError> {
sqlx::query!(
r#"
INSERT INTO af_related_template_view (view_id, related_view_id)
SELECT $1 AS view_id, related_view_id
FROM UNNEST($2::uuid[]) AS t(related_view_id)
"#,
view_id,
category_ids
)
.execute(executor)
.await?;
Ok(())
}
pub async fn delete_related_templates<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
) -> Result<(), AppError> {
sqlx::query!(
r#"
DELETE FROM af_related_template_view
WHERE view_id = $1
"#,
view_id,
)
.execute(executor)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn insert_template_view<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
name: &str,
description: &str,
about: &str,
view_url: &str,
creator_id: Uuid,
is_new_template: bool,
is_featured: bool,
) -> Result<(), AppError> {
sqlx::query!(
r#"
INSERT INTO af_template_view (
view_id,
name,
description,
about,
view_url,
creator_id,
is_new_template,
is_featured
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
view_id,
name,
description,
about,
view_url,
creator_id,
is_new_template,
is_featured
)
.execute(executor)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn update_template_view<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
name: &str,
description: &str,
about: &str,
view_url: &str,
creator_id: Uuid,
is_new_template: bool,
is_featured: bool,
) -> Result<(), AppError> {
sqlx::query!(
r#"
UPDATE af_template_view SET
updated_at = NOW(),
name = $2,
description = $3,
about = $4,
view_url = $5,
creator_id = $6,
is_new_template = $7,
is_featured = $8
WHERE view_id = $1
"#,
view_id,
name,
description,
about,
view_url,
creator_id,
is_new_template,
is_featured
)
.execute(executor)
.await?;
Ok(())
}
pub async fn select_template_view_by_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
) -> Result<Template, AppError> {
let view_row = sqlx::query_as!(
AFTemplateRow,
r#"
WITH template_with_creator_account_link AS (
SELECT
template.view_id,
template.creator_id,
COALESCE(
ARRAY_AGG((link_type, url)::account_link_type) FILTER (WHERE link_type IS NOT NULL),
'{}'
) AS account_links
FROM af_template_view template
JOIN af_template_creator creator
USING (creator_id)
LEFT OUTER JOIN af_template_creator_account_link account_link
USING (creator_id)
WHERE view_id = $1
GROUP BY (view_id, template.creator_id)
),
related_template_with_category AS (
SELECT
template.related_view_id,
ARRAY_AGG(
(
template_category.category_id,
template_category.name,
template_category.icon,
template_category.bg_color
)::template_category_minimal_type
) AS categories
FROM af_related_template_view template
JOIN af_template_view_template_category template_template_category
ON template.related_view_id = template_template_category.view_id
JOIN af_template_category template_category
USING (category_id)
WHERE template.view_id = $1
GROUP BY template.related_view_id
),
template_with_related_template AS (
SELECT
template.view_id,
ARRAY_AGG(
(
template.related_view_id,
related_template.created_at,
related_template.updated_at,
related_template.name,
related_template.description,
related_template.view_url,
(
creator.creator_id,
creator.name,
creator.avatar_url
)::template_creator_minimal_type,
related_template_with_category.categories,
related_template.is_new_template,
related_template.is_featured
)::template_minimal_type
) AS related_templates
FROM af_related_template_view template
JOIN af_template_view related_template
ON template.related_view_id = related_template.view_id
JOIN af_template_creator creator
ON related_template.creator_id = creator.creator_id
JOIN related_template_with_category
ON template.related_view_id = related_template_with_category.related_view_id
WHERE template.view_id = $1
GROUP BY template.view_id
),
template_with_category AS (
SELECT
view_id,
COALESCE(
ARRAY_AGG((
vtc.category_id,
name,
icon,
bg_color,
description,
category_type,
priority
)) FILTER (WHERE vtc.category_id IS NOT NULL),
'{}'
) AS categories
FROM af_template_view_template_category vtc
JOIN af_template_category tc
ON vtc.category_id = tc.category_id
WHERE view_id = $1
GROUP BY view_id
),
creator_number_of_templates AS (
SELECT
creator_id,
COUNT(*) AS number_of_templates
FROM af_template_view
GROUP BY creator_id
)
SELECT
template.view_id,
template.created_at,
template.updated_at,
template.name,
template.description,
template.about,
template.view_url,
(
creator.creator_id,
creator.name,
creator.avatar_url,
template_with_creator_account_link.account_links,
creator_number_of_templates.number_of_templates
)::template_creator_type AS "creator!: AFTemplateCreatorRow",
template_with_category.categories AS "categories!: Vec<AFTemplateCategoryRow>",
COALESCE(template_with_related_template.related_templates, '{}') AS "related_templates!: Vec<AFTemplateMinimalRow>",
template.is_new_template,
template.is_featured
FROM af_template_view template
JOIN af_template_creator creator
USING (creator_id)
JOIN template_with_creator_account_link
ON template.view_id = template_with_creator_account_link.view_id
LEFT OUTER JOIN template_with_related_template
ON template.view_id = template_with_related_template.view_id
JOIN template_with_category
ON template.view_id = template_with_category.view_id
LEFT OUTER JOIN creator_number_of_templates
ON template.creator_id = creator_number_of_templates.creator_id
WHERE template.view_id = $1
"#,
view_id
)
.fetch_one(executor)
.await?;
let view = view_row.into();
Ok(view)
}
pub async fn select_templates<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
category_id: Option<Uuid>,
is_featured: Option<bool>,
is_new_template: Option<bool>,
name_contains: Option<&str>,
limit: Option<i64>,
) -> Result<Vec<TemplateMinimal>, AppError> {
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
r#"
WITH template_with_template_category AS (
SELECT
template_template_category.view_id,
ARRAY_AGG((
template_template_category.category_id,
category.name,
category.icon,
category.bg_color
)::template_category_minimal_type) AS categories
FROM af_template_view_template_category template_template_category
JOIN af_template_category category
USING (category_id)
JOIN af_template_view template
USING (view_id)
WHERE TRUE
"#,
);
if let Some(category_id) = category_id {
query_builder.push(" AND template_template_category.category_id = ");
query_builder.push_bind(category_id);
};
if let Some(is_featured) = is_featured {
query_builder.push(" AND template.is_featured = ");
query_builder.push_bind(is_featured);
};
if let Some(is_new_template) = is_new_template {
query_builder.push(" AND template.is_new_template = ");
query_builder.push_bind(is_new_template);
};
if let Some(name_contains) = name_contains {
query_builder.push(" AND template.name ILIKE CONCAT('%', ");
query_builder.push_bind(name_contains);
query_builder.push(" , '%')");
};
query_builder.push(
r#"
GROUP BY template_template_category.view_id
)
SELECT
template.view_id,
template.created_at,
template.updated_at,
template.name,
template.description,
template.view_url,
(
template_creator.creator_id,
template_creator.name,
template_creator.avatar_url
)::template_creator_minimal_type AS creator,
tc.categories AS categories,
template.is_new_template,
template.is_featured
FROM template_with_template_category tc
JOIN af_template_view template
USING (view_id)
JOIN af_template_creator template_creator
USING (creator_id)
ORDER BY template.created_at DESC
"#,
);
if let Some(limit) = limit {
query_builder.push(" LIMIT ");
query_builder.push_bind(limit);
};
let query = query_builder.build_query_as::<AFTemplateMinimalRow>();
let template_rows: Vec<AFTemplateMinimalRow> = query.fetch_all(executor).await?;
Ok(template_rows.into_iter().map(|row| row.into()).collect())
}
pub async fn select_template_homepage<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
per_count: i64,
) -> Result<Vec<TemplateGroup>, AppError> {
let template_group_rows = sqlx::query_as!(
AFTemplateGroupRow,
r#"
WITH recent_template AS (
SELECT
template_template_category.category_id,
template_template_category.view_id,
category.name,
category.icon,
category.bg_color,
ROW_NUMBER() OVER (PARTITION BY template_template_category.category_id ORDER BY template.created_at DESC) AS recency
FROM af_template_view_template_category template_template_category
JOIN af_template_category category
USING (category_id)
JOIN af_template_view template
USING (view_id)
),
template_group_by_category_and_view AS (
SELECT
category_id,
view_id,
ARRAY_AGG((
category_id,
name,
icon,
bg_color
)::template_category_minimal_type) AS categories
FROM recent_template
WHERE recency <= $1
GROUP BY category_id, view_id
),
template_group_by_category_and_view_with_creator_and_template_details AS (
SELECT
template_group_by_category_and_view.category_id,
(
template.view_id,
template.created_at,
template.updated_at,
template.name,
template.description,
template.view_url,
(
creator.creator_id,
creator.name,
creator.avatar_url
)::template_creator_minimal_type,
template_group_by_category_and_view.categories,
template.is_new_template,
template.is_featured
)::template_minimal_type AS template
FROM template_group_by_category_and_view
JOIN af_template_view template
USING (view_id)
JOIN af_template_creator creator
USING (creator_id)
),
template_group_by_category AS (
SELECT
category_id,
ARRAY_AGG(template) AS templates
FROM template_group_by_category_and_view_with_creator_and_template_details
GROUP BY category_id
)
SELECT
(
template_group_by_category.category_id,
category.name,
category.icon,
category.bg_color
)::template_category_minimal_type AS "category!: AFTemplateCategoryMinimalRow",
templates AS "templates!: Vec<AFTemplateMinimalRow>"
FROM template_group_by_category
JOIN af_template_category category
USING (category_id)
"#,
per_count,
)
.fetch_all(executor)
.await?;
Ok(
template_group_rows
.into_iter()
.map(|row| row.into())
.collect(),
)
}
pub async fn delete_template_by_view_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: Uuid,
) -> Result<(), AppError> {
sqlx::query!(
r#"
DELETE FROM af_template_view
WHERE view_id = $1
"#,
view_id,
)
.execute(executor)
.await?;
Ok(())
}

View file

@ -0,0 +1,85 @@
-- Appflowy template is based on a published view. Template information should be preserved even if the
-- published view is deleted.
CREATE TABLE IF NOT EXISTS af_template_view (
view_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
name TEXT NOT NULL,
description TEXT NOT NULL,
about TEXT NOT NULL,
view_url TEXT NOT NULL,
creator_id UUID NOT NULL REFERENCES af_template_creator(creator_id) ON DELETE CASCADE,
is_new_template BOOLEAN NOT NULL,
is_featured BOOLEAN NOT NULL,
PRIMARY KEY (view_id)
);
CREATE TABLE IF NOT EXISTS af_template_view_template_category (
view_id UUID NOT NULL REFERENCES af_template_view(view_id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES af_template_category(category_id) ON DELETE CASCADE,
PRIMARY KEY (view_id, category_id)
);
CREATE TABLE IF NOT EXISTS af_related_template_view (
view_id UUID NOT NULL REFERENCES af_template_view(view_id) ON DELETE CASCADE,
related_view_id UUID NOT NULL REFERENCES af_template_view(view_id) ON DELETE CASCADE,
PRIMARY KEY (view_id, related_view_id)
);
BEGIN;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'account_link_type') THEN
CREATE TYPE account_link_type AS (
link_type TEXT,
url TEXT
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'template_creator_type') THEN
CREATE TYPE template_creator_type AS (
creator_id UUID,
name TEXT,
avatar_url TEXT,
account_links account_link_type[],
number_of_templates INT
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'template_creator_minimal_type') THEN
CREATE TYPE template_creator_minimal_type AS (
creator_id UUID,
name TEXT,
avatar_url TEXT
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'template_category_minimal_type') THEN
CREATE TYPE template_category_minimal_type AS (
category_id UUID,
name TEXT,
icon TEXT,
bg_color TEXT
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'template_minimal_type') THEN
CREATE TYPE template_minimal_type AS (
view_id UUID,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
name TEXT,
description TEXT,
view_url TEXT,
creator template_creator_minimal_type,
categories template_category_minimal_type[],
is_new_template BOOLEAN,
is_featured BOOLEAN
);
END IF;
END
$$;

View file

@ -3,21 +3,16 @@ use actix_web::{
Result, Scope,
};
use database_entity::dto::{
CreateTemplateCategoryParams, CreateTemplateCreatorParams, GetTemplateCategoriesQueryParams,
GetTemplateCreatorsQueryParams, TemplateCategories, TemplateCategory, TemplateCreator,
TemplateCreators, UpdateTemplateCategoryParams, UpdateTemplateCreatorParams,
CreateTemplateCategoryParams, CreateTemplateCreatorParams, CreateTemplateParams,
GetTemplateCategoriesQueryParams, GetTemplateCreatorsQueryParams, GetTemplatesQueryParams,
Template, TemplateCategories, TemplateCategory, TemplateCreator, TemplateCreators,
TemplateHomePage, TemplateHomePageQueryParams, Templates, UpdateTemplateCategoryParams,
UpdateTemplateCreatorParams, UpdateTemplateParams,
};
use shared_entity::response::{AppResponse, JsonAppResponse};
use uuid::Uuid;
use crate::{
biz::template::ops::{
create_new_template_category, create_new_template_creator, delete_template_category,
delete_template_creator, get_template_categories, get_template_category, get_template_creator,
get_template_creators, update_template_category, update_template_creator,
},
state::AppState,
};
use crate::{biz::template::ops::*, state::AppState};
pub fn template_scope() -> Scope {
web::scope("/api/template-center")
@ -43,6 +38,18 @@ pub fn template_scope() -> Scope {
.route(web::get().to(get_template_creator_handler))
.route(web::delete().to(delete_template_creator_handler)),
)
.service(
web::resource("/template")
.route(web::post().to(post_template_handler))
.route(web::get().to(list_templates_handler)),
)
.service(
web::resource("/template/{view_id}")
.route(web::put().to(update_template_handler))
.route(web::get().to(get_template_handler))
.route(web::delete().to(delete_template_handler)),
)
.service(web::resource("/homepage").route(web::get().to(get_template_homepage_handler)))
}
async fn post_template_category_handler(
@ -173,3 +180,91 @@ async fn delete_template_creator_handler(
delete_template_creator(&state.pg_pool, creator_id).await?;
Ok(Json(AppResponse::Ok()))
}
async fn post_template_handler(
data: Json<CreateTemplateParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<Template>> {
let new_template = create_new_template(
&state.pg_pool,
data.view_id,
&data.name,
&data.description,
&data.about,
&data.view_url,
data.creator_id,
data.is_new_template,
data.is_featured,
&data.category_ids,
&data.related_view_ids,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(new_template)))
}
async fn list_templates_handler(
data: web::Query<GetTemplatesQueryParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<Templates>> {
let data = data.into_inner();
let template_summary_list = get_templates(
&state.pg_pool,
data.category_id,
data.is_featured,
data.is_new_template,
data.name_contains.as_deref(),
)
.await?;
Ok(Json(AppResponse::Ok().with_data(Templates {
templates: template_summary_list,
})))
}
async fn get_template_handler(
view_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<JsonAppResponse<Template>> {
let view_id = view_id.into_inner();
let template = get_template(&state.pg_pool, view_id).await?;
Ok(Json(AppResponse::Ok().with_data(template)))
}
async fn update_template_handler(
view_id: web::Path<Uuid>,
data: Json<UpdateTemplateParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<Template>> {
let view_id = view_id.into_inner();
let updated_template = update_template(
&state.pg_pool,
view_id,
&data.name,
&data.description,
&data.about,
&data.view_url,
data.creator_id,
data.is_new_template,
data.is_featured,
&data.category_ids,
&data.related_view_ids,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(updated_template)))
}
async fn delete_template_handler(
view_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<JsonAppResponse<()>> {
let view_id = view_id.into_inner();
delete_template(&state.pg_pool, view_id).await?;
Ok(Json(AppResponse::Ok()))
}
async fn get_template_homepage_handler(
query: web::Query<TemplateHomePageQueryParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<TemplateHomePage>> {
let template_homepage = get_template_homepage(&state.pg_pool, query.per_count).await?;
Ok(Json(AppResponse::Ok().with_data(template_homepage)))
}

View file

@ -1,13 +1,11 @@
use std::ops::DerefMut;
use anyhow::Context;
use database::template::{
delete_template_category_by_id, delete_template_creator_account_links,
delete_template_creator_by_id, insert_new_template_category, insert_template_creator,
select_template_categories, select_template_category_by_id, select_template_creator_by_id,
select_template_creators_by_name, update_template_category_by_id, update_template_creator_by_id,
use database::template::*;
use database_entity::dto::{
AccountLink, Template, TemplateCategory, TemplateCategoryType, TemplateCreator, TemplateHomePage,
TemplateMinimal,
};
use database_entity::dto::{AccountLink, TemplateCategory, TemplateCategoryType, TemplateCreator};
use shared_entity::response::AppResponseError;
use sqlx::PgPool;
use uuid::Uuid;
@ -141,3 +139,134 @@ pub async fn delete_template_creator(
delete_template_creator_by_id(pg_pool, creator_id).await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn create_new_template(
pg_pool: &PgPool,
view_id: Uuid,
name: &str,
description: &str,
about: &str,
view_url: &str,
creator_id: Uuid,
is_new_template: bool,
is_featured: bool,
category_ids: &[Uuid],
related_view_ids: &[Uuid],
) -> Result<Template, AppResponseError> {
let mut txn = pg_pool
.begin()
.await
.context("Begin transaction to create template creator")?;
insert_template_view(
txn.deref_mut(),
view_id,
name,
description,
about,
view_url,
creator_id,
is_new_template,
is_featured,
)
.await?;
insert_template_view_template_category(txn.deref_mut(), view_id, category_ids).await?;
insert_related_templates(txn.deref_mut(), view_id, related_view_ids).await?;
let new_template = select_template_view_by_id(txn.deref_mut(), view_id).await?;
txn
.commit()
.await
.context("Commit transaction to update template creator")?;
Ok(new_template)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_template(
pg_pool: &PgPool,
view_id: Uuid,
name: &str,
description: &str,
about: &str,
view_url: &str,
creator_id: Uuid,
is_new_template: bool,
is_featured: bool,
category_ids: &[Uuid],
related_view_ids: &[Uuid],
) -> Result<Template, AppResponseError> {
let mut txn = pg_pool
.begin()
.await
.context("Begin transaction to update template")?;
delete_template_view_template_categories(txn.deref_mut(), view_id).await?;
delete_related_templates(txn.deref_mut(), view_id).await?;
update_template_view(
txn.deref_mut(),
view_id,
name,
description,
about,
view_url,
creator_id,
is_new_template,
is_featured,
)
.await?;
insert_template_view_template_category(txn.deref_mut(), view_id, category_ids).await?;
insert_related_templates(txn.deref_mut(), view_id, related_view_ids).await?;
let updated_template = select_template_view_by_id(txn.deref_mut(), view_id).await?;
txn
.commit()
.await
.context("Commit transaction to update template")?;
Ok(updated_template)
}
pub async fn get_templates(
pg_pool: &PgPool,
category_id: Option<Uuid>,
is_featured: Option<bool>,
is_new_template: Option<bool>,
name_contains: Option<&str>,
) -> Result<Vec<TemplateMinimal>, AppResponseError> {
let templates = select_templates(
pg_pool,
category_id,
is_featured,
is_new_template,
name_contains,
None,
)
.await?;
Ok(templates)
}
pub async fn get_template(pg_pool: &PgPool, view_id: Uuid) -> Result<Template, AppResponseError> {
let template = select_template_view_by_id(pg_pool, view_id).await?;
Ok(template)
}
pub async fn delete_template(pg_pool: &PgPool, view_id: Uuid) -> Result<(), AppResponseError> {
delete_template_by_view_id(pg_pool, view_id).await?;
Ok(())
}
const DEFAULT_HOMEPAGE_CATEGORY_COUNT: i64 = 10;
pub async fn get_template_homepage(
pg_pool: &PgPool,
per_count: Option<i64>,
) -> Result<TemplateHomePage, AppResponseError> {
let per_count = per_count.unwrap_or(DEFAULT_HOMEPAGE_CATEGORY_COUNT);
let template_groups = select_template_homepage(pg_pool, per_count).await?;
let featured_templates =
select_templates(pg_pool, None, Some(true), None, None, Some(per_count)).await?;
let new_templates =
select_templates(pg_pool, None, None, Some(true), None, Some(per_count)).await?;
let homepage = TemplateHomePage {
template_groups,
featured_templates,
new_templates,
};
Ok(homepage)
}

View file

@ -1,6 +1,9 @@
use std::collections::HashSet;
use app_error::ErrorCode;
use client_api::entity::{
AccountLink, CreateTemplateCategoryParams, TemplateCategoryType, UpdateTemplateCategoryParams,
AccountLink, CreateTemplateCategoryParams, CreateTemplateParams, PublishCollabItem,
PublishCollabMetadata, TemplateCategoryType, UpdateTemplateCategoryParams, UpdateTemplateParams,
};
use client_api_test::*;
use collab::core::collab::DataSource;
@ -10,6 +13,16 @@ use collab_entity::CollabType;
use database_entity::dto::{QueryCollab, QueryCollabParams};
use uuid::Uuid;
async fn get_first_workspace_string(c: &client_api::Client) -> String {
c.get_workspaces()
.await
.unwrap()
.first()
.unwrap()
.workspace_id
.to_string()
}
#[tokio::test]
async fn get_user_default_workspace_test() {
let email = generate_unique_email();
@ -178,11 +191,13 @@ async fn test_template_creator_crud() {
link_type: "reddit".to_string(),
url: "reddit_url".to_string(),
}];
let creator_name_prefix = Uuid::new_v4().to_string();
let creator_name = format!("{}-name", creator_name_prefix);
let new_creator = authorized_client
.create_template_creator("name", "avatar_url", account_links)
.create_template_creator(creator_name.as_str(), "avatar_url", account_links)
.await
.unwrap();
assert_eq!(new_creator.name, "name");
assert_eq!(new_creator.name, creator_name);
assert_eq!(new_creator.avatar_url, "avatar_url");
assert_eq!(new_creator.account_links.len(), 1);
assert_eq!(new_creator.account_links[0].link_type, "reddit");
@ -197,16 +212,17 @@ async fn test_template_creator_crud() {
link_type: "twitter".to_string(),
url: "twitter_url".to_string(),
}];
let updated_creator_name = format!("{}-new_name", creator_name_prefix);
let updated_creator = authorized_client
.update_template_creator(
new_creator.id,
"new_name",
updated_creator_name.as_str(),
"new_avatar_url",
updated_account_links,
)
.await
.unwrap();
assert_eq!(updated_creator.name, "new_name");
assert_eq!(updated_creator.name, updated_creator_name);
assert_eq!(updated_creator.avatar_url, "new_avatar_url");
assert_eq!(updated_creator.account_links.len(), 1);
assert_eq!(updated_creator.account_links[0].link_type, "twitter");
@ -216,12 +232,19 @@ async fn test_template_creator_crud() {
.get_template_creator(new_creator.id)
.await
.unwrap();
assert_eq!(creator.name, "new_name");
assert_eq!(creator.name, updated_creator_name);
assert_eq!(creator.avatar_url, "new_avatar_url");
assert_eq!(creator.account_links.len(), 1);
assert_eq!(creator.account_links[0].link_type, "twitter");
assert_eq!(creator.account_links[0].url, "twitter_url");
let creators = guest_client
.get_template_creators(Some(creator_name_prefix).as_deref())
.await
.unwrap()
.creators;
assert_eq!(creators.len(), 1);
let result = guest_client.delete_template_creator(new_creator.id).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn);
@ -233,3 +256,215 @@ async fn test_template_creator_crud() {
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, ErrorCode::RecordNotFound);
}
#[tokio::test]
async fn test_template_crud() {
let (authorized_client, _) = generate_unique_registered_user_client().await;
let workspace_id = get_first_workspace_string(&authorized_client).await;
let published_view_namespace = uuid::Uuid::new_v4().to_string();
authorized_client
.set_workspace_publish_namespace(&workspace_id.to_string(), &published_view_namespace)
.await
.unwrap();
let published_view_ids: Vec<Uuid> = (0..4).map(|_| Uuid::new_v4()).collect();
let published_collab_items: Vec<PublishCollabItem<TemplateMetadata, &[u8]>> = published_view_ids
.iter()
.map(|view_id| PublishCollabItem {
meta: PublishCollabMetadata {
view_id: *view_id,
publish_name: view_id.to_string(),
metadata: TemplateMetadata {},
},
data: "yrs_encoded_data_1".as_bytes(),
})
.collect();
authorized_client
.publish_collabs::<TemplateMetadata, &[u8]>(&workspace_id, published_collab_items)
.await
.unwrap();
let category_prefix = Uuid::new_v4().to_string();
let category_1_name = format!("{}_1", category_prefix);
let category_2_name = format!("{}_2", category_prefix);
let creator_1 = authorized_client
.create_template_creator(
"template_creator 1",
"avatar_url",
vec![AccountLink {
link_type: "reddit".to_string(),
url: "reddit_url".to_string(),
}],
)
.await
.unwrap();
let creator_2 = authorized_client
.create_template_creator(
"template_creator 2",
"avatar_url",
vec![AccountLink {
link_type: "facebook".to_string(),
url: "facebook_url".to_string(),
}],
)
.await
.unwrap();
let params = CreateTemplateCategoryParams {
name: category_1_name,
icon: "icon".to_string(),
bg_color: "bg_color".to_string(),
description: "description".to_string(),
category_type: TemplateCategoryType::Feature,
priority: 0,
};
let category_1 = authorized_client
.create_template_category(&params)
.await
.unwrap();
let params = CreateTemplateCategoryParams {
name: category_2_name,
icon: "icon".to_string(),
bg_color: "bg_color".to_string(),
description: "description".to_string(),
category_type: TemplateCategoryType::Feature,
priority: 0,
};
let category_2 = authorized_client
.create_template_category(&params)
.await
.unwrap();
let template_name_prefix = Uuid::new_v4().to_string();
for (index, view_id) in published_view_ids[0..2].iter().enumerate() {
let is_new_template = index % 2 == 0;
let is_featured = true;
let category_id = category_1.id;
let params = CreateTemplateParams {
view_id: *view_id,
name: format!("{}-{}", template_name_prefix, view_id),
description: "description".to_string(),
about: "about".to_string(),
view_url: "view_url".to_string(),
category_ids: vec![category_id],
creator_id: creator_1.id,
is_new_template,
is_featured,
related_view_ids: vec![],
};
let template = authorized_client.create_template(&params).await.unwrap();
assert_eq!(template.view_id, *view_id);
assert_eq!(template.categories.len(), 1);
assert_eq!(template.categories[0].id, category_id);
assert_eq!(template.creator.id, creator_1.id);
assert_eq!(template.creator.name, creator_1.name);
assert_eq!(template.creator.account_links.len(), 1);
assert_eq!(
template.creator.account_links[0].url,
creator_1.account_links[0].url
);
assert!(template.related_templates.is_empty())
}
for (index, view_id) in published_view_ids[2..4].iter().enumerate() {
let is_new_template = index % 2 == 0;
let is_featured = false;
let category_id = category_2.id;
let params = CreateTemplateParams {
view_id: *view_id,
name: format!("{}-{}", template_name_prefix, view_id),
description: "description".to_string(),
about: "about".to_string(),
view_url: "view_url".to_string(),
category_ids: vec![category_id],
creator_id: creator_2.id,
is_new_template,
is_featured,
related_view_ids: vec![published_view_ids[0]],
};
let template = authorized_client.create_template(&params).await.unwrap();
assert_eq!(template.related_templates.len(), 1);
assert_eq!(template.related_templates[0].view_id, published_view_ids[0]);
assert_eq!(template.related_templates[0].creator.id, creator_1.id);
assert_eq!(template.related_templates[0].categories.len(), 1);
assert_eq!(
template.related_templates[0].categories[0].id,
category_1.id
);
}
let guest_client = localhost_client();
let templates = guest_client
.get_templates(
Some(category_2.id),
None,
None,
Some(template_name_prefix.clone()),
)
.await
.unwrap()
.templates;
let view_ids: HashSet<Uuid> = templates.iter().map(|t| t.view_id).collect();
assert_eq!(templates.len(), 2);
assert!(view_ids.contains(&published_view_ids[2]));
assert!(view_ids.contains(&published_view_ids[3]));
let featured_templates = guest_client
.get_templates(None, Some(true), None, Some(template_name_prefix.clone()))
.await
.unwrap()
.templates;
let featured_view_ids: HashSet<Uuid> = featured_templates.iter().map(|t| t.view_id).collect();
assert_eq!(featured_templates.len(), 2);
assert!(featured_view_ids.contains(&published_view_ids[0]));
assert!(featured_view_ids.contains(&published_view_ids[1]));
let new_templates = guest_client
.get_templates(None, None, Some(true), Some(template_name_prefix.clone()))
.await
.unwrap()
.templates;
let new_view_ids: HashSet<Uuid> = new_templates.iter().map(|t| t.view_id).collect();
assert_eq!(new_templates.len(), 2);
assert!(new_view_ids.contains(&published_view_ids[0]));
assert!(new_view_ids.contains(&published_view_ids[2]));
let template = guest_client
.get_template(published_view_ids[3])
.await
.unwrap();
assert_eq!(template.view_id, published_view_ids[3]);
assert_eq!(template.creator.id, creator_2.id);
assert_eq!(template.categories.len(), 1);
assert_eq!(template.categories[0].id, category_2.id);
assert_eq!(template.related_templates.len(), 1);
assert_eq!(template.related_templates[0].view_id, published_view_ids[0]);
let params = UpdateTemplateParams {
name: format!("{}-{}", template_name_prefix, published_view_ids[3]),
description: "description".to_string(),
about: "about".to_string(),
view_url: "view_url".to_string(),
category_ids: vec![category_1.id],
creator_id: creator_2.id,
is_new_template: false,
is_featured: true,
related_view_ids: vec![published_view_ids[0]],
};
authorized_client
.update_template(published_view_ids[3], &params)
.await
.unwrap();
authorized_client
.delete_template(published_view_ids[3])
.await
.unwrap();
let resp = guest_client.get_template(published_view_ids[3]).await;
assert!(resp.is_err());
assert_eq!(resp.unwrap_err().code, ErrorCode::RecordNotFound);
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TemplateMetadata {}