From 34cff1868b94325e1a16549cad5c1d2d632da4ea Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Wed, 8 Jan 2025 18:19:22 +0100 Subject: [PATCH 01/10] feat: add typesafe api client --- core/.env.test | 2 + .../site/[...ts-rest]/route.ts | 2 +- core/lib/server/pubtype.ts | 25 +- core/package.json | 2 +- packages/contracts/src/resources/site.ts | 421 ++++++++++-------- packages/platform-sdk/CHANGELOG.md | 7 + packages/platform-sdk/README.md | 1 + packages/platform-sdk/package.json | 44 ++ .../platform-sdk/src/.generated-pubtypes.ts | 416 +++++++++++++++++ .../__snapshots__/type-generator.test.ts.snap | 421 ++++++++++++++++++ packages/platform-sdk/src/client.ts | 29 ++ packages/platform-sdk/src/index.ts | 4 + .../platform-sdk/src/type-generator.test.ts | 363 +++++++++++++++ packages/platform-sdk/src/type-generator.ts | 306 +++++++++++++ packages/platform-sdk/src/types.ts | 3 + packages/platform-sdk/tsconfig.json | 10 + packages/platform-sdk/vitest.config.mts | 17 + packages/schemas/src/index.ts | 61 ++- packages/schemas/src/schemas.ts | 6 + pnpm-lock.yaml | 187 +++++--- 20 files changed, 2039 insertions(+), 288 deletions(-) create mode 100644 packages/platform-sdk/CHANGELOG.md create mode 100644 packages/platform-sdk/README.md create mode 100644 packages/platform-sdk/package.json create mode 100644 packages/platform-sdk/src/.generated-pubtypes.ts create mode 100644 packages/platform-sdk/src/__snapshots__/type-generator.test.ts.snap create mode 100644 packages/platform-sdk/src/client.ts create mode 100644 packages/platform-sdk/src/index.ts create mode 100644 packages/platform-sdk/src/type-generator.test.ts create mode 100644 packages/platform-sdk/src/type-generator.ts create mode 100644 packages/platform-sdk/src/types.ts create mode 100644 packages/platform-sdk/tsconfig.json create mode 100644 packages/platform-sdk/vitest.config.mts diff --git a/core/.env.test b/core/.env.test index d6cccd24b7..1958a7642d 100644 --- a/core/.env.test +++ b/core/.env.test @@ -15,3 +15,5 @@ OTEL_SERVICE_NAME="pubpub-v7-dev" # should be shared across components but not e HONEYCOMB_API_KEY="xxx" KYSELY_DEBUG="true" + +GCLOUD_KEY_FILE="xxx" \ No newline at end of file diff --git a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts index da95772c92..d7b64576e7 100644 --- a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts +++ b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts @@ -227,7 +227,7 @@ const handler = createNextHandler( cookies: false, }); - const { pubTypeId, stageId, ...rest } = query; + const { pubTypeId, stageId, ...rest } = query ?? {}; const pubs = await getPubsWithRelatedValuesAndChildren( { diff --git a/core/lib/server/pubtype.ts b/core/lib/server/pubtype.ts index fad65499a7..e199bdc4f4 100644 --- a/core/lib/server/pubtype.ts +++ b/core/lib/server/pubtype.ts @@ -1,11 +1,12 @@ import type { ExpressionBuilder } from "kysely"; import { sql } from "kysely"; -import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/postgres"; +import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/postgres"; +import type { PubTypeWithFields } from "contracts/src/resources/site"; import type { CommunitiesId, FormsId, PubFieldsId, PubsId, PubTypesId } from "db/public"; -import type { Prettify, XOR } from "../types"; +import type { AutoReturnType, Equal, Expect, Prettify, XOR } from "../types"; import type { GetManyParams } from "./pub"; import { db } from "~/kysely/database"; import { autoCache } from "./cache/autoCache"; @@ -32,21 +33,6 @@ export const getPubTypeBase = >( "pub_fields.schemaName", "pub_fields.isRelation", "_PubFieldToPubType.isTitle", - jsonObjectFrom( - eb - .selectFrom("PubFieldSchema") - .select([ - "PubFieldSchema.id", - "PubFieldSchema.namespace", - "PubFieldSchema.name", - "PubFieldSchema.schema", - ]) - .whereRef( - "PubFieldSchema.id", - "=", - eb.ref("pub_fields.pubFieldSchemaId") - ) - ).as("schema"), ]) .where("_PubFieldToPubType.B", "=", eb.ref("pub_types.id")) ).as("fields"), @@ -55,6 +41,11 @@ export const getPubTypeBase = >( export const getPubType = (pubTypeId: PubTypesId) => autoCache(getPubTypeBase().where("pub_types.id", "=", pubTypeId)); +// can't really assert an autoCache return type, so we'll just do this +type _TestPubType = Expect< + Equal["executeTakeFirstOrThrow"], PubTypeWithFields> +>; + export const getPubTypeForPubId = async (pubId: PubsId) => { return autoCache( getPubTypeBase() diff --git a/core/package.json b/core/package.json index 1fc36f6d88..898d404f82 100644 --- a/core/package.json +++ b/core/package.json @@ -181,7 +181,7 @@ "tsx": "catalog:", "typescript": "catalog:", "vite-tsconfig-paths": "^5.0.1", - "vitest": "^2.1.1", + "vitest": "^2.1.8", "yargs": "^17.7.2" }, "prettier": "@pubpub/prettier-config" diff --git a/packages/contracts/src/resources/site.ts b/packages/contracts/src/resources/site.ts index f39bc645db..8f3ae5418f 100644 --- a/packages/contracts/src/resources/site.ts +++ b/packages/contracts/src/resources/site.ts @@ -1,3 +1,5 @@ +import type { Prettify } from "@ts-rest/core"; + import { initContract } from "@ts-rest/core"; import { z } from "zod"; @@ -247,9 +249,19 @@ export type ProcessedPub = ProcessedPubBas MaybePubMembers & MaybePubLegacyAssignee; +export type PubTypeWithFields = Prettify< + PubTypes & { + fields: Prettify< + Pick & { + isTitle: boolean; + } + >[]; + } +>; + export interface NonGenericProcessedPub extends ProcessedPubBase { stage?: Stages | null; - pubType?: PubTypes; + pubType?: PubTypeWithFields; children?: NonGenericProcessedPub[]; values?: (ValueBase & { relatedPub?: NonGenericProcessedPub | null; @@ -257,6 +269,23 @@ export interface NonGenericProcessedPub extends ProcessedPubBase { })[]; } +const pubTypeReturnSchema = pubTypesSchema.extend({ + fields: z.array( + pubFieldsSchema + .omit({ + createdAt: true, + updatedAt: true, + communityId: true, + isArchived: true, + integrationId: true, + pubFieldSchemaId: true, + }) + .extend({ + isTitle: z.boolean(), + }) + ), +}) satisfies z.ZodType; + const processedPubSchema: z.ZodType = z.object({ id: pubsIdSchema, stageId: stagesIdSchema.nullable(), @@ -279,11 +308,7 @@ const processedPubSchema: z.ZodType = z.object({ createdAt: z.date(), updatedAt: z.date(), stage: stagesSchema.nullish(), - pubType: pubTypesSchema - .extend({ - fields: z.array(pubFieldsSchema), - }) - .optional(), + pubType: pubTypeReturnSchema.optional(), children: z.lazy(() => z.array(processedPubSchema)).optional(), assignee: usersSchema.nullish(), }); @@ -327,208 +352,236 @@ const getPubQuerySchema = z }) .passthrough(); -export const siteApi = contract.router( - { - pubs: { - get: { - method: "GET", - path: "/pubs/:pubId", - summary: "Gets a pub", - description: - "Get a pub and its children by ID. This endpoint is used by the PubPub site builder to get a pub's details.", - pathParams: z.object({ - pubId: z.string().uuid(), - }), - query: getPubQuerySchema, - responses: { - 200: processedPubSchema, - }, - }, - getMany: { - method: "GET", - path: "/pubs", - summary: "Gets a list of pubs", - description: - "Get a list of pubs by ID. This endpoint is used by the PubPub site builder to get a list of pubs.", - query: getPubQuerySchema.extend({ - pubTypeId: pubTypesIdSchema.optional().describe("Filter by pub type ID."), - stageId: stagesIdSchema.optional().describe("Filter by stage ID."), - limit: z.number().default(10), - offset: z.number().default(0).optional(), - orderBy: z.enum(["createdAt", "updatedAt"]).optional(), - orderDirection: z.enum(["asc", "desc"]).optional(), - }), - responses: { - 200: z.array(processedPubSchema), - }, - }, - create: { - summary: "Creates a pub", - description: "Creates a pub.", - method: "POST", - path: "/pubs", - headers: preferRepresentationHeaderSchema, - body: CreatePubRequestBodyWithNullsNew, - responses: { - 201: processedPubSchema, - 204: z.never().optional(), - }, - }, - update: { - summary: "Updates a pub", - description: "Updates a pubs values.", - method: "PATCH", - path: "/pubs/:pubId", - headers: preferRepresentationHeaderSchema, - body: z.record(jsonSchema), - responses: { - 200: processedPubSchema, - 204: z.never().optional(), - }, - }, - archive: { - summary: "Archives a pub", - description: "Archives a pub by ID.", - method: "DELETE", - body: z.never().nullish(), - path: "/pubs/:pubId", - responses: { - 204: z.never().optional(), - 404: z.literal("Pub not found"), +export interface CommunitySpecificTypes { + Pub?: {}; + PubType?: {}; +} + +type MaybePub = C extends { + Pub: infer P; +} + ? P + : NonGenericProcessedPub; + +type MaybePubType = C extends { + PubType: infer P; +} + ? P + : z.infer; + +export const createSiteApi = < + C extends CommunitySpecificTypes = CommunitySpecificTypes, + Pub extends MaybePub = MaybePub, + PubType extends MaybePubType = MaybePubType, +>() => + contract.router( + { + pubs: { + get: { + method: "GET", + path: "/pubs/:pubId", + summary: "Gets a pub", + description: + "Get a pub and its children by ID. This endpoint is used by the PubPub site builder to get a pub's details.", + pathParams: z.object({ + pubId: z.string().uuid(), + }), + query: getPubQuerySchema.optional(), + responses: { + 200: processedPubSchema as unknown as z.ZodType, + }, }, - }, - relations: { - update: { - summary: "Update pub relation fields", + getMany: { + method: "GET", + path: "/pubs", + summary: "Gets a list of pubs", description: - "Updates pub relations for the specified slugs. Only adds or modifies specified relations, leaves existing relations alone. If you want to replace all relations for a field, use PUT.", - method: "PATCH", - path: "/pubs/:pubId/relations", + "Get a list of pubs by ID. This endpoint is used by the PubPub site builder to get a list of pubs.", + query: getPubQuerySchema + .extend({ + pubTypeId: pubTypesIdSchema + .optional() + .describe("Filter by pub type ID."), + stageId: stagesIdSchema.optional().describe("Filter by stage ID."), + limit: z.number().default(10), + offset: z.number().default(0).optional(), + orderBy: z.enum(["createdAt", "updatedAt"]).optional(), + orderDirection: z.enum(["asc", "desc"]).optional(), + }) + .optional(), + responses: { + 200: z.array(processedPubSchema) as unknown as z.ZodType, + }, + }, + create: { + summary: "Creates a pub", + description: "Creates a pub.", + method: "POST", + path: "/pubs", headers: preferRepresentationHeaderSchema, - body: upsertPubRelationsSchema, + body: CreatePubRequestBodyWithNullsNew, responses: { - 200: processedPubSchema, + 201: processedPubSchema as unknown as z.ZodType, 204: z.never().optional(), }, }, - replace: { - summary: "Replace pub relation fields", - description: - "Replaces all pub relations for the specified slugs. If you want to add or modify relations without overwriting existing ones, use PATCH.", - method: "PUT", - path: "/pubs/:pubId/relations", + update: { + summary: "Updates a pub", + description: "Updates a pubs values.", + method: "PATCH", + path: "/pubs/:pubId", headers: preferRepresentationHeaderSchema, - body: upsertPubRelationsSchema, + body: z.record(jsonSchema), responses: { - 200: processedPubSchema, + 200: processedPubSchema as unknown as z.ZodType, 204: z.never().optional(), }, }, - remove: { - summary: "Remove pub relation fields", - description: - "Removes related pubs from the specified pubfields. Provide a dictionary with field slugs as keys and arrays of pubIds to remove as values. Use '*' to remove all relations for a given field slug.\n Note: This endpoint does not remove the related pubs themselves, only the relations.", + archive: { + summary: "Archives a pub", + description: "Archives a pub by ID.", method: "DELETE", - path: "/pubs/:pubId/relations", - headers: preferRepresentationHeaderSchema, - body: z.record(z.union([z.literal("*"), z.array(pubsIdSchema)])), + body: z.never().nullish(), + path: "/pubs/:pubId", responses: { - 200: processedPubSchema, 204: z.never().optional(), + 404: z.literal("Pub not found"), }, }, - }, - }, - pubTypes: { - get: { - path: "/pub-types/:pubTypeId", - method: "GET", - summary: "Gets a pub type", - description: - "Get a pub type by ID. This endpoint is used by the PubPub site builder to get a pub type's details.", - pathParams: z.object({ - pubTypeId: z.string().uuid(), - }), - responses: { - 200: pubTypesSchema, + relations: { + update: { + summary: "Update pub relation fields", + description: + "Updates pub relations for the specified slugs. Only adds or modifies specified relations, leaves existing relations alone. If you want to replace all relations for a field, use PUT.", + method: "PATCH", + path: "/pubs/:pubId/relations", + headers: preferRepresentationHeaderSchema, + body: upsertPubRelationsSchema, + responses: { + 200: processedPubSchema as unknown as z.ZodType, + 204: z.never().optional(), + }, + }, + replace: { + summary: "Replace pub relation fields", + description: + "Replaces all pub relations for the specified slugs. If you want to add or modify relations without overwriting existing ones, use PATCH.", + method: "PUT", + path: "/pubs/:pubId/relations", + headers: preferRepresentationHeaderSchema, + body: upsertPubRelationsSchema, + responses: { + 200: processedPubSchema as unknown as z.ZodType, + 204: z.never().optional(), + }, + }, + remove: { + summary: "Remove pub relation fields", + description: + "Removes related pubs from the specified pubfields. Provide a dictionary with field slugs as keys and arrays of pubIds to remove as values. Use '*' to remove all relations for a given field slug.\n Note: This endpoint does not remove the related pubs themselves, only the relations.", + method: "DELETE", + path: "/pubs/:pubId/relations", + headers: preferRepresentationHeaderSchema, + body: z.record(z.union([z.literal("*"), z.array(pubsIdSchema)])), + responses: { + 200: processedPubSchema as unknown as z.ZodType, + 204: z.never().optional(), + }, + }, }, }, - getMany: { - path: "/pub-types", - method: "GET", - summary: "Gets a list of pub types", - description: - "Get a list of pub types by ID. This endpoint is used by the PubPub site builder to get a list of pub types.", - query: z.object({ - limit: z.number().default(10), - offset: z.number().default(0).optional(), - orderBy: z.enum(["createdAt", "updatedAt"]).optional(), - orderDirection: z.enum(["asc", "desc"]).optional(), - }), - responses: { - 200: pubTypesSchema.array(), + pubTypes: { + get: { + path: "/pub-types/:pubTypeId", + method: "GET", + summary: "Gets a pub type", + description: + "Get a pub type by ID. This endpoint is used by the PubPub site builder to get a pub type's details.", + pathParams: z.object({ + pubTypeId: z.string().uuid(), + }), + responses: { + 200: pubTypeReturnSchema as unknown as z.ZodType, + }, }, - }, - }, - stages: { - get: { - path: "/stages/:stageId", - method: "GET", - summary: "Gets a stage", - description: - "Get a stage by ID. This endpoint is used by the PubPub site builder to get a stage's details.", - pathParams: z.object({ - stageId: z.string().uuid(), - }), - responses: { - 200: stagesSchema, + getMany: { + path: "/pub-types", + method: "GET", + summary: "Gets a list of pub types", + description: + "Get a list of pub types by ID. This endpoint is used by the PubPub site builder to get a list of pub types.", + query: z.object({ + limit: z.number().default(10), + offset: z.number().default(0).optional(), + orderBy: z.enum(["createdAt", "updatedAt"]).optional(), + orderDirection: z.enum(["asc", "desc"]).optional(), + }), + responses: { + 200: z.array(pubTypeReturnSchema) as unknown as z.ZodType, + }, }, }, - getMany: { - path: "/stages", - method: "GET", - summary: "Gets a list of stages", - description: - "Get a list of stages by ID. This endpoint is used by the PubPub site builder to get a list of stages.", - query: z.object({ - limit: z.number().default(10), - offset: z.number().default(0).optional(), - orderBy: z.enum(["createdAt", "updatedAt"]).optional(), - orderDirection: z.enum(["asc", "desc"]).optional(), - }), - responses: { - 200: stagesSchema.array(), + stages: { + get: { + path: "/stages/:stageId", + method: "GET", + summary: "Gets a stage", + description: + "Get a stage by ID. This endpoint is used by the PubPub site builder to get a stage's details.", + pathParams: z.object({ + stageId: z.string().uuid(), + }), + responses: { + 200: stagesSchema, + }, + }, + getMany: { + path: "/stages", + method: "GET", + summary: "Gets a list of stages", + description: + "Get a list of stages by ID. This endpoint is used by the PubPub site builder to get a list of stages.", + query: z.object({ + limit: z.number().default(10), + offset: z.number().default(0).optional(), + orderBy: z.enum(["createdAt", "updatedAt"]).optional(), + orderDirection: z.enum(["asc", "desc"]).optional(), + }), + responses: { + 200: stagesSchema.array(), + }, }, }, - }, - users: { - search: { - path: "/users/search", - method: "GET", - summary: "Get a list of matching users for autocomplete", - description: - "Get a list of users matching the provided query. Used for rendering suggestions in an autocomplete input for selecting users.", - query: z.object({ - communityId: communitiesIdSchema, - email: z.string(), - name: z.string().optional(), - limit: z.number().optional(), - }), - responses: { - 200: safeUserSchema - .extend({ member: communityMembershipsSchema.nullable().optional() }) - .array(), + users: { + search: { + path: "/users/search", + method: "GET", + summary: "Get a list of matching users for autocomplete", + description: + "Get a list of users matching the provided query. Used for rendering suggestions in an autocomplete input for selecting users.", + query: z.object({ + communityId: communitiesIdSchema, + email: z.string(), + name: z.string().optional(), + limit: z.number().optional(), + }), + responses: { + 200: safeUserSchema + .extend({ member: communityMembershipsSchema.nullable().optional() }) + .array(), + }, }, }, }, - }, - { - pathPrefix: "/api/v0/c/:communitySlug/site", - baseHeaders: z.object({ - authorization: z - .string() - .regex(/^Bearer /) - .optional(), - }), - } -); + { + pathPrefix: "/api/v0/c/:communitySlug/site", + baseHeaders: z.object({ + authorization: z + .string() + .regex(/^Bearer /) + .optional(), + }), + } + ); + +export const siteApi = createSiteApi(); diff --git a/packages/platform-sdk/CHANGELOG.md b/packages/platform-sdk/CHANGELOG.md new file mode 100644 index 0000000000..d7dca66867 --- /dev/null +++ b/packages/platform-sdk/CHANGELOG.md @@ -0,0 +1,7 @@ +# @pubpub/integration-sdk + +## 0.0.6 + +### Patch Changes + +- b0da2c3: This release adds wrapping to the input group fields and fixes button styles. diff --git a/packages/platform-sdk/README.md b/packages/platform-sdk/README.md new file mode 100644 index 0000000000..b05a381ceb --- /dev/null +++ b/packages/platform-sdk/README.md @@ -0,0 +1 @@ +# @pubpub/platform-sdk diff --git a/packages/platform-sdk/package.json b/packages/platform-sdk/package.json new file mode 100644 index 0000000000..6ed6c9021f --- /dev/null +++ b/packages/platform-sdk/package.json @@ -0,0 +1,44 @@ +{ + "name": "@pubpub/platform-sdk", + "private": true, + "version": "0.0.1", + "description": "SDK for the PubPub platform", + "main": "dist/pubpub-platform-sdk.cjs.js", + "module": "dist/pubpub-platform-sdk.esm.js", + "files": [ + "dist" + ], + "scripts": { + "format": "prettier --check . --ignore-path ../../.gitignore", + "format:fix": "prettier -w . --ignore-path ../../.gitignore", + "type-check": "tsc --noEmit", + "test": "vitest" + }, + "keywords": [ + "pubpub", + "api", + "sdk" + ], + "author": "Knowledge Futures, Inc ", + "license": "GPL-2.0+", + "dependencies": { + "@ts-rest/core": "catalog:", + "contracts": "workspace:*", + "db": "workspace:*", + "utils": "workspace:*" + }, + "devDependencies": { + "@pubpub/prettier-config": "workspace:*", + "@types/node": "catalog:", + "tsconfig": "workspace:*", + "typescript": "catalog:", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.8" + }, + "preconstruct": { + "entrypoints": [ + "index.ts" + ] + }, + "prettier": "@pubpub/prettier-config" +} diff --git a/packages/platform-sdk/src/.generated-pubtypes.ts b/packages/platform-sdk/src/.generated-pubtypes.ts new file mode 100644 index 0000000000..c193cf9f61 --- /dev/null +++ b/packages/platform-sdk/src/.generated-pubtypes.ts @@ -0,0 +1,416 @@ +// Generated by PubPub Type Generator +import type { CoreSchemaType, PubFieldsId, PubTypesId, CommunitiesId, StagesId, PubsId } from "db/public"; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +export interface PubTypeField { + id: PubFieldsId; + name: string; + slug: string; + schemaName: CoreSchemaType; + isRelation: boolean; + isTitle: boolean; +} + +export interface PubTypeDefinition { + createdAt: Date; + updatedAt: Date; + id: PubTypesId; + communityId: CommunitiesId; + name: string; + description: string | null; + fields: PubTypeField[]; +} + +export type SchemaNameToTypeMap = { + [CoreSchemaType.Boolean]: boolean; + [CoreSchemaType.DateTime]: Date; + [CoreSchemaType.Email]: string; + [CoreSchemaType.MemberId]: string; + [CoreSchemaType.Number]: number; + [CoreSchemaType.NumericArray]: number[]; + [CoreSchemaType.RichText]: { type: "doc"; content: unknown[] }; + [CoreSchemaType.String]: string; + [CoreSchemaType.StringArray]: string[]; + [CoreSchemaType.Vector3]: [number, number, number]; + [CoreSchemaType.URL]: string; + [CoreSchemaType.Null]: null; + [CoreSchemaType.FileUpload]: { + id: string; + name: string; + type: string; + size: number; + url: string; + }; +} + +export type PubField_title = { + id: string; + name: "Title"; + slug: "test-community:title"; + schemaName: CoreSchemaType.String; + isRelation: false; + isTitle: true; +} + +export type PubField_some_relation = { + id: string; + name: "Some relation"; + slug: "test-community:some-relation"; + schemaName: CoreSchemaType.String; + isRelation: true; + isTitle: false; +} + +export type PubField_ok = { + id: string; + name: "Ok"; + slug: "test-community:ok"; + schemaName: CoreSchemaType.Boolean; + isRelation: false; + isTitle: false; +} + +export type PubField_number = { + id: string; + name: "Number"; + slug: "test-community:number"; + schemaName: CoreSchemaType.Number; + isRelation: false; + isTitle: false; +} + +export type PubField_date_time = { + id: string; + name: "Date Time"; + slug: "test-community:date-time"; + schemaName: CoreSchemaType.DateTime; + isRelation: false; + isTitle: false; +} + +export type PubField_url = { + id: string; + name: "Url"; + slug: "test-community:url"; + schemaName: CoreSchemaType.URL; + isRelation: false; + isTitle: false; +} + +export type PubField_email = { + id: string; + name: "Email"; + slug: "test-community:email"; + schemaName: CoreSchemaType.Email; + isRelation: false; + isTitle: false; +} + +export type PubField_vector3 = { + id: string; + name: "Vector3"; + slug: "test-community:vector3"; + schemaName: CoreSchemaType.Vector3; + isRelation: false; + isTitle: false; +} + +export type PubField_numeric_array = { + id: string; + name: "Numeric Array"; + slug: "test-community:numeric-array"; + schemaName: CoreSchemaType.NumericArray; + isRelation: false; + isTitle: false; +} + +export type PubField_string_array = { + id: string; + name: "String Array"; + slug: "test-community:string-array"; + schemaName: CoreSchemaType.StringArray; + isRelation: false; + isTitle: false; +} + +export type PubField_rich_text = { + id: string; + name: "Rich Text"; + slug: "test-community:rich-text"; + schemaName: CoreSchemaType.RichText; + isRelation: false; + isTitle: false; +} + +export type PubField_member = { + id: string; + name: "Member"; + slug: "test-community:member"; + schemaName: CoreSchemaType.MemberId; + isRelation: false; + isTitle: false; +} + +export type PubField_fun_relation = { + id: string; + name: "Fun Relation"; + slug: "test-community:fun-relation"; + schemaName: CoreSchemaType.Null; + isRelation: true; + isTitle: false; +}; + +export type PubField = PubField_title | PubField_some_relation | PubField_ok | PubField_number | PubField_date_time | PubField_url | PubField_email | PubField_vector3 | PubField_numeric_array | PubField_string_array | PubField_rich_text | PubField_member | PubField_fun_relation; + +export type PubType_Basic_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Basic Pub"; + description: null; + fields: (PubField_title | PubField_some_relation)[]; + } + +export type PubType_Minimal_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Minimal Pub"; + description: null; + fields: (PubField_title)[]; + } + +export type PubType_Everything_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Everything Pub"; + description: null; + fields: (PubField_title | PubField_some_relation | PubField_ok | PubField_number | PubField_date_time | PubField_url | PubField_email | PubField_vector3 | PubField_numeric_array | PubField_string_array | PubField_rich_text | PubField_member | PubField_fun_relation)[]; + }; + +export type PubType = PubType_Basic_Pub | PubType_Minimal_Pub | PubType_Everything_Pub + +export type PubFieldToValueType = Prettify<{ + id: PubFieldsId; + createdAt: Date; + updatedAt: Date; + fieldSlug: T["slug"]; + fieldName: T["name"]; + schemaName: T["schemaName"]; + value: SchemaNameToTypeMap[T["schemaName"]]; +} & (T["isRelation"] extends true ? { + relatedPubId: PubTypesId | null; + relatedPub: Pub | null; + } : { + relatedPubId: null; + relatedPub?: never; +})> + +export type Pub_Basic_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Basic Pub"; + pubType: PubType_Basic_Pub; + values: (PubFieldToValueType | PubFieldToValueType)[] +} + +export type Pub_Minimal_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Minimal Pub"; + pubType: PubType_Minimal_Pub; + values: (PubFieldToValueType)[] +} + +export type Pub_Everything_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Everything Pub"; + pubType: PubType_Everything_Pub; + values: (PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType)[] +} + +export type Pub = Pub_Basic_Pub | Pub_Minimal_Pub | Pub_Everything_Pub + + +export interface CommunitySchema { + pubTypes: { + "Basic Pub": { + id: "590c7a05-d35b-49eb-8f36-af99a7705be8", + name: "Basic Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + }, + "test-community:some-relation": { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: CoreSchemaType.String, + isRelation: true, + isTitle: false + } + } + }, + "Minimal Pub": { + id: "6365dfe7-9839-4d88-baab-a7d479510acd", + name: "Minimal Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + } + } + }, + "Everything Pub": { + id: "a979056f-6b85-4aa3-9a8d-78855fdad956", + name: "Everything Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + }, + "test-community:some-relation": { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: CoreSchemaType.String, + isRelation: true, + isTitle: false + }, + "test-community:ok": { + id: "53713c81-66f2-446e-8164-8d4d419a978b", + name: "Ok", + slug: "test-community:ok", + schemaName: CoreSchemaType.Boolean, + isRelation: false, + isTitle: false + }, + "test-community:number": { + id: "7cac2f42-dd4d-4d1f-81e9-dd34c88bdeb1", + name: "Number", + slug: "test-community:number", + schemaName: CoreSchemaType.Number, + isRelation: false, + isTitle: false + }, + "test-community:date-time": { + id: "a496f1d2-17c8-4525-b7cd-8d4f42529564", + name: "Date Time", + slug: "test-community:date-time", + schemaName: CoreSchemaType.DateTime, + isRelation: false, + isTitle: false + }, + "test-community:url": { + id: "6c248f98-0535-4ca8-b8f2-31e665e481e6", + name: "Url", + slug: "test-community:url", + schemaName: CoreSchemaType.URL, + isRelation: false, + isTitle: false + }, + "test-community:email": { + id: "8b81da18-021b-4737-9a91-c13e924a9667", + name: "Email", + slug: "test-community:email", + schemaName: CoreSchemaType.Email, + isRelation: false, + isTitle: false + }, + "test-community:vector3": { + id: "382d81f1-1c5d-4a75-b79d-2adc4784854c", + name: "Vector3", + slug: "test-community:vector3", + schemaName: CoreSchemaType.Vector3, + isRelation: false, + isTitle: false + }, + "test-community:numeric-array": { + id: "4afbd428-3c5d-4766-815c-7f5a74a34404", + name: "Numeric Array", + slug: "test-community:numeric-array", + schemaName: CoreSchemaType.NumericArray, + isRelation: false, + isTitle: false + }, + "test-community:string-array": { + id: "4142263b-7d5d-4652-a8e3-562a690309fe", + name: "String Array", + slug: "test-community:string-array", + schemaName: CoreSchemaType.StringArray, + isRelation: false, + isTitle: false + }, + "test-community:rich-text": { + id: "86f1d589-ce2c-45c8-978d-2907beb495ac", + name: "Rich Text", + slug: "test-community:rich-text", + schemaName: CoreSchemaType.RichText, + isRelation: false, + isTitle: false + }, + "test-community:member": { + id: "47c32939-02f3-4e32-9b5b-51484c20d82b", + name: "Member", + slug: "test-community:member", + schemaName: CoreSchemaType.MemberId, + isRelation: false, + isTitle: false + }, + "test-community:fun-relation": { + id: "fc498e7a-ef06-463e-aed5-83a329fb0b07", + name: "Fun Relation", + slug: "test-community:fun-relation", + schemaName: CoreSchemaType.Null, + isRelation: true, + isTitle: false + } + } + } + } +} diff --git a/packages/platform-sdk/src/__snapshots__/type-generator.test.ts.snap b/packages/platform-sdk/src/__snapshots__/type-generator.test.ts.snap new file mode 100644 index 0000000000..3d8812c877 --- /dev/null +++ b/packages/platform-sdk/src/__snapshots__/type-generator.test.ts.snap @@ -0,0 +1,421 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`exportPubTypes > should be able to export pub types 1`] = ` +"// Generated by PubPub Type Generator +import type { CoreSchemaType, PubFieldsId, PubTypesId, CommunitiesId, StagesId, PubsId } from "db/public"; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +export interface PubTypeField { + id: PubFieldsId; + name: string; + slug: string; + schemaName: CoreSchemaType; + isRelation: boolean; + isTitle: boolean; +} + +export interface PubTypeDefinition { + createdAt: Date; + updatedAt: Date; + id: PubTypesId; + communityId: CommunitiesId; + name: string; + description: string | null; + fields: PubTypeField[]; +} + +export type SchemaNameToTypeMap = { + [CoreSchemaType.Boolean]: boolean; + [CoreSchemaType.DateTime]: Date; + [CoreSchemaType.Email]: string; + [CoreSchemaType.MemberId]: string; + [CoreSchemaType.Number]: number; + [CoreSchemaType.NumericArray]: number[]; + [CoreSchemaType.RichText]: { type: "doc"; content: unknown[] }; + [CoreSchemaType.String]: string; + [CoreSchemaType.StringArray]: string[]; + [CoreSchemaType.Vector3]: [number, number, number]; + [CoreSchemaType.URL]: string; + [CoreSchemaType.Null]: null; + [CoreSchemaType.FileUpload]: { + id: string; + name: string; + type: string; + size: number; + url: string; + }; +} + +export type PubField_title = { + id: string; + name: "Title"; + slug: "test-community:title"; + schemaName: CoreSchemaType.String; + isRelation: false; + isTitle: true; +} + +export type PubField_some_relation = { + id: string; + name: "Some relation"; + slug: "test-community:some-relation"; + schemaName: CoreSchemaType.String; + isRelation: true; + isTitle: false; +} + +export type PubField_ok = { + id: string; + name: "Ok"; + slug: "test-community:ok"; + schemaName: CoreSchemaType.Boolean; + isRelation: false; + isTitle: false; +} + +export type PubField_number = { + id: string; + name: "Number"; + slug: "test-community:number"; + schemaName: CoreSchemaType.Number; + isRelation: false; + isTitle: false; +} + +export type PubField_date_time = { + id: string; + name: "Date Time"; + slug: "test-community:date-time"; + schemaName: CoreSchemaType.DateTime; + isRelation: false; + isTitle: false; +} + +export type PubField_url = { + id: string; + name: "Url"; + slug: "test-community:url"; + schemaName: CoreSchemaType.URL; + isRelation: false; + isTitle: false; +} + +export type PubField_email = { + id: string; + name: "Email"; + slug: "test-community:email"; + schemaName: CoreSchemaType.Email; + isRelation: false; + isTitle: false; +} + +export type PubField_vector3 = { + id: string; + name: "Vector3"; + slug: "test-community:vector3"; + schemaName: CoreSchemaType.Vector3; + isRelation: false; + isTitle: false; +} + +export type PubField_numeric_array = { + id: string; + name: "Numeric Array"; + slug: "test-community:numeric-array"; + schemaName: CoreSchemaType.NumericArray; + isRelation: false; + isTitle: false; +} + +export type PubField_string_array = { + id: string; + name: "String Array"; + slug: "test-community:string-array"; + schemaName: CoreSchemaType.StringArray; + isRelation: false; + isTitle: false; +} + +export type PubField_rich_text = { + id: string; + name: "Rich Text"; + slug: "test-community:rich-text"; + schemaName: CoreSchemaType.RichText; + isRelation: false; + isTitle: false; +} + +export type PubField_member = { + id: string; + name: "Member"; + slug: "test-community:member"; + schemaName: CoreSchemaType.MemberId; + isRelation: false; + isTitle: false; +} + +export type PubField_fun_relation = { + id: string; + name: "Fun Relation"; + slug: "test-community:fun-relation"; + schemaName: CoreSchemaType.Null; + isRelation: true; + isTitle: false; +}; + +export type PubField = PubField_title | PubField_some_relation | PubField_ok | PubField_number | PubField_date_time | PubField_url | PubField_email | PubField_vector3 | PubField_numeric_array | PubField_string_array | PubField_rich_text | PubField_member | PubField_fun_relation; + +export type PubType_Basic_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Basic Pub"; + description: null; + fields: (PubField_title | PubField_some_relation)[]; + } + +export type PubType_Minimal_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Minimal Pub"; + description: null; + fields: (PubField_title)[]; + } + +export type PubType_Everything_Pub = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "Everything Pub"; + description: null; + fields: (PubField_title | PubField_some_relation | PubField_ok | PubField_number | PubField_date_time | PubField_url | PubField_email | PubField_vector3 | PubField_numeric_array | PubField_string_array | PubField_rich_text | PubField_member | PubField_fun_relation)[]; + }; + +export type PubType = PubType_Basic_Pub | PubType_Minimal_Pub | PubType_Everything_Pub + +export type PubFieldToValueType = Prettify<{ + id: PubFieldsId; + createdAt: Date; + updatedAt: Date; + fieldSlug: T["slug"]; + fieldName: T["name"]; + schemaName: T["schemaName"]; + value: SchemaNameToTypeMap[T["schemaName"]]; +} & (T["isRelation"] extends true ? { + relatedPubId: PubTypesId | null; + relatedPub: Pub | null; + } : { + relatedPubId: null; + relatedPub?: never; +})> + +export type Pub_Basic_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Basic Pub"; + pubType: PubType_Basic_Pub; + values: (PubFieldToValueType | PubFieldToValueType)[] +} + +export type Pub_Minimal_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Minimal Pub"; + pubType: PubType_Minimal_Pub; + values: (PubFieldToValueType)[] +} + +export type Pub_Everything_Pub = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "Everything Pub"; + pubType: PubType_Everything_Pub; + values: (PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType | PubFieldToValueType)[] +} + +export type Pub = Pub_Basic_Pub | Pub_Minimal_Pub | Pub_Everything_Pub + + +export interface CommunitySchema { + pubTypes: { + "Basic Pub": { + id: "590c7a05-d35b-49eb-8f36-af99a7705be8", + name: "Basic Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + }, + "test-community:some-relation": { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: CoreSchemaType.String, + isRelation: true, + isTitle: false + } + } + }, + "Minimal Pub": { + id: "6365dfe7-9839-4d88-baab-a7d479510acd", + name: "Minimal Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + } + } + }, + "Everything Pub": { + id: "a979056f-6b85-4aa3-9a8d-78855fdad956", + name: "Everything Pub", + description: null, + fields: { + "test-community:title": { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: CoreSchemaType.String, + isRelation: false, + isTitle: true + }, + "test-community:some-relation": { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: CoreSchemaType.String, + isRelation: true, + isTitle: false + }, + "test-community:ok": { + id: "53713c81-66f2-446e-8164-8d4d419a978b", + name: "Ok", + slug: "test-community:ok", + schemaName: CoreSchemaType.Boolean, + isRelation: false, + isTitle: false + }, + "test-community:number": { + id: "7cac2f42-dd4d-4d1f-81e9-dd34c88bdeb1", + name: "Number", + slug: "test-community:number", + schemaName: CoreSchemaType.Number, + isRelation: false, + isTitle: false + }, + "test-community:date-time": { + id: "a496f1d2-17c8-4525-b7cd-8d4f42529564", + name: "Date Time", + slug: "test-community:date-time", + schemaName: CoreSchemaType.DateTime, + isRelation: false, + isTitle: false + }, + "test-community:url": { + id: "6c248f98-0535-4ca8-b8f2-31e665e481e6", + name: "Url", + slug: "test-community:url", + schemaName: CoreSchemaType.URL, + isRelation: false, + isTitle: false + }, + "test-community:email": { + id: "8b81da18-021b-4737-9a91-c13e924a9667", + name: "Email", + slug: "test-community:email", + schemaName: CoreSchemaType.Email, + isRelation: false, + isTitle: false + }, + "test-community:vector3": { + id: "382d81f1-1c5d-4a75-b79d-2adc4784854c", + name: "Vector3", + slug: "test-community:vector3", + schemaName: CoreSchemaType.Vector3, + isRelation: false, + isTitle: false + }, + "test-community:numeric-array": { + id: "4afbd428-3c5d-4766-815c-7f5a74a34404", + name: "Numeric Array", + slug: "test-community:numeric-array", + schemaName: CoreSchemaType.NumericArray, + isRelation: false, + isTitle: false + }, + "test-community:string-array": { + id: "4142263b-7d5d-4652-a8e3-562a690309fe", + name: "String Array", + slug: "test-community:string-array", + schemaName: CoreSchemaType.StringArray, + isRelation: false, + isTitle: false + }, + "test-community:rich-text": { + id: "86f1d589-ce2c-45c8-978d-2907beb495ac", + name: "Rich Text", + slug: "test-community:rich-text", + schemaName: CoreSchemaType.RichText, + isRelation: false, + isTitle: false + }, + "test-community:member": { + id: "47c32939-02f3-4e32-9b5b-51484c20d82b", + name: "Member", + slug: "test-community:member", + schemaName: CoreSchemaType.MemberId, + isRelation: false, + isTitle: false + }, + "test-community:fun-relation": { + id: "fc498e7a-ef06-463e-aed5-83a329fb0b07", + name: "Fun Relation", + slug: "test-community:fun-relation", + schemaName: CoreSchemaType.Null, + isRelation: true, + isTitle: false + } + } + } + } +} +" +`; diff --git a/packages/platform-sdk/src/client.ts b/packages/platform-sdk/src/client.ts new file mode 100644 index 0000000000..4a10522c58 --- /dev/null +++ b/packages/platform-sdk/src/client.ts @@ -0,0 +1,29 @@ +import { initClient } from "@ts-rest/core"; + +import type { CommunitySpecificTypes, NonGenericProcessedPub } from "contracts"; +import { createSiteApi } from "contracts"; + +export const makeClient = (baseUrl: string) => { + const api = createSiteApi(); + // do some other modifications, such as replacing the communitySlug with the actual community slug + // so you don't constantly have to pass it in + + return initClient(api, { + baseUrl: baseUrl, + }); +}; + +export const isPubOfType = < + T extends { + pubType: { + name: string; + }; + values: any[]; + }, + PubTypeName extends T["pubType"]["name"], +>( + pub: T, + pubType: PubTypeName +): pub is Extract => { + return pub.pubType.name === pubType; +}; diff --git a/packages/platform-sdk/src/index.ts b/packages/platform-sdk/src/index.ts new file mode 100644 index 0000000000..d0dc9c5caa --- /dev/null +++ b/packages/platform-sdk/src/index.ts @@ -0,0 +1,4 @@ +export * from "./client"; + +export type { PubsId, PubTypesId, PubFieldsId, CommunitiesId, StagesId } from "db/public"; +export { CoreSchemaType } from "db/public"; diff --git a/packages/platform-sdk/src/type-generator.test.ts b/packages/platform-sdk/src/type-generator.test.ts new file mode 100644 index 0000000000..49eb8c21e4 --- /dev/null +++ b/packages/platform-sdk/src/type-generator.test.ts @@ -0,0 +1,363 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import type { PubFields, PubTypes, PubTypesId } from "db/public"; +import { CoreSchemaType } from "db/public"; + +import type { Pub, PubType } from "./.generated-pubtypes"; +import { isPubOfType, makeClient } from "./client"; +import { generateTypeDefinitions } from "./type-generator"; + +export const pubTypeStub = [ + { + id: "590c7a05-d35b-49eb-8f36-af99a7705be8", + description: null, + name: "Basic Pub", + communityId: "0a17cfb2-c369-4f29-8dfd-92a51bb0ee71", + createdAt: new Date(), + updatedAt: new Date(), + fields: [ + { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: "String", + isRelation: false, + isTitle: true, + schema: null, + }, + { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: "String", + isRelation: true, + isTitle: false, + schema: null, + }, + ], + }, + { + id: "6365dfe7-9839-4d88-baab-a7d479510acd", + description: null, + name: "Minimal Pub", + communityId: "0a17cfb2-c369-4f29-8dfd-92a51bb0ee71", + createdAt: new Date(), + updatedAt: new Date(), + fields: [ + { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: "String", + isRelation: false, + isTitle: true, + schema: null, + }, + ], + }, + { + id: "a979056f-6b85-4aa3-9a8d-78855fdad956", + description: null, + name: "Everything Pub", + communityId: "0a17cfb2-c369-4f29-8dfd-92a51bb0ee71", + createdAt: new Date(), + updatedAt: new Date(), + fields: [ + { + id: "1a2e321a-c42a-4f24-b9cf-e66368382fdc", + name: "Title", + slug: "test-community:title", + schemaName: "String", + isRelation: false, + isTitle: true, + schema: null, + }, + { + id: "c6003a3c-7fc3-488d-8220-2f467ce4ff36", + name: "Some relation", + slug: "test-community:some-relation", + schemaName: "String", + isRelation: true, + isTitle: false, + schema: null, + }, + { + id: "53713c81-66f2-446e-8164-8d4d419a978b", + name: "Ok", + slug: "test-community:ok", + schemaName: "Boolean", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "7cac2f42-dd4d-4d1f-81e9-dd34c88bdeb1", + name: "Number", + slug: "test-community:number", + schemaName: "Number", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "a496f1d2-17c8-4525-b7cd-8d4f42529564", + name: "Date Time", + slug: "test-community:date-time", + schemaName: "DateTime", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "6c248f98-0535-4ca8-b8f2-31e665e481e6", + name: "Url", + slug: "test-community:url", + schemaName: "URL", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "8b81da18-021b-4737-9a91-c13e924a9667", + name: "Email", + slug: "test-community:email", + schemaName: "Email", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "382d81f1-1c5d-4a75-b79d-2adc4784854c", + name: "Vector3", + slug: "test-community:vector3", + schemaName: "Vector3", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "4afbd428-3c5d-4766-815c-7f5a74a34404", + name: "Numeric Array", + slug: "test-community:numeric-array", + schemaName: "NumericArray", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "4142263b-7d5d-4652-a8e3-562a690309fe", + name: "String Array", + slug: "test-community:string-array", + schemaName: "StringArray", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "86f1d589-ce2c-45c8-978d-2907beb495ac", + name: "Rich Text", + slug: "test-community:rich-text", + schemaName: "RichText", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "47c32939-02f3-4e32-9b5b-51484c20d82b", + name: "Member", + slug: "test-community:member", + schemaName: "MemberId", + isRelation: false, + isTitle: false, + schema: null, + }, + { + id: "fc498e7a-ef06-463e-aed5-83a329fb0b07", + name: "Fun Relation", + slug: "test-community:fun-relation", + schemaName: "Null", + isRelation: true, + isTitle: false, + schema: null, + }, + ], + }, +] as unknown as (PubTypes & { fields: (PubFields & { isTitle: true })[] })[]; + +describe("exportPubTypes", () => { + it("should be able to export pub types", async () => { + const path = new URL(".generated-pubtypes.ts", import.meta.url).pathname; + + const pubTypes = await generateTypeDefinitions(pubTypeStub, path); + + expect(pubTypes).toMatchSnapshot(); + }); + + it("hngg", async () => { + let pub = {} as unknown as Pub; + + if (pub.pubTypeName === "Everything Pub") { + pub.values; + } + }); +}); + +declare const client: ReturnType>; + +const pubTypeNames = ["Basic Pub", "Minimal Pub", "Everything Pub"] as const; + +describe("type tests", () => { + describe("pub", () => { + it("test pub Get return ", async () => { + type PubGetReturn = Awaited>; + + const pub = {} as unknown as PubGetReturn; + + if (pub.status !== 200) { + // for get body is only defined on 200 + expectTypeOf(pub.body).toEqualTypeOf(); + return; + } + + expectTypeOf(pub.body.pubTypeName).toMatchTypeOf<(typeof pubTypeNames)[number]>(); + + if (pub.body.pubType.name === "Basic Pub") { + pub.body.values.map((value) => { + // this type narrowing shouldn't work + expectTypeOf(value.fieldName).not.toEqualTypeOf<"Title" | "Some relation">(); + }); + } + + // you need to use the type guard to get the type narrowing + if (isPubOfType(pub.body, "Basic Pub")) { + pub.body.values.map((value) => { + expectTypeOf(value.fieldName).toEqualTypeOf<"Title" | "Some relation">(); + }); + } else if (isPubOfType(pub.body, "Everything Pub")) { + pub.body.values.map((value) => { + expectTypeOf(value.fieldName).toEqualTypeOf< + | "Title" + | "Some relation" + | "Ok" + | "Number" + | "Date Time" + | "Url" + | "Email" + | "Vector3" + | "Numeric Array" + | "String Array" + | "Rich Text" + | "Member" + | "Fun Relation" + >(); + + // this tests whether you can get the type of a field by their fieldname + switch (value.fieldName) { + case "Title": + expectTypeOf(value.value).toEqualTypeOf(); + // non relation fields should not have a relatedPub or relatedPubId + expectTypeOf(value.relatedPub).toEqualTypeOf(); + expectTypeOf(value.relatedPubId).toEqualTypeOf(); + break; + case "Some relation": + // relations should have a relatedPubId and possibly a relatedPub + expectTypeOf(value.relatedPubId).toEqualTypeOf(); + expectTypeOf(value.relatedPub).toEqualTypeOf(); + break; + case "Ok": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Number": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Date Time": + // hmm, maybe this should be a string, unless we hydrate values on the client + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Url": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Email": + expectTypeOf(value.value).toEqualTypeOf(); + break; + + case "Vector3": + expectTypeOf(value.value).toEqualTypeOf<[number, number, number]>(); + break; + case "Numeric Array": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "String Array": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Rich Text": + expectTypeOf(value.value).toEqualTypeOf<{ + content: unknown[]; + type: "doc"; + }>(); + break; + case "Member": + expectTypeOf(value.value).toEqualTypeOf(); + break; + case "Fun Relation": + expectTypeOf(value.value).toEqualTypeOf(); + expectTypeOf(value.relatedPubId).toEqualTypeOf(); + expectTypeOf(value.relatedPub).toEqualTypeOf(); + break; + + default: + const _exhaustiveCheck: never = value; + break; + } + + // this checks whether you can get the type of a field by its CoreSchematype + switch (value.schemaName) { + case CoreSchemaType.Boolean: + expectTypeOf(value.value).toEqualTypeOf(); + expectTypeOf(value.fieldName).toEqualTypeOf<"Ok">(); + break; + case CoreSchemaType.String: + expectTypeOf(value.value).toEqualTypeOf(); + expectTypeOf(value.fieldName).toEqualTypeOf< + "Title" | "Some relation" + >(); + break; + // don't feel like testing everything here, it's mostly the same as above + default: + break; + } + + switch (value.fieldSlug) { + case "test-community:numeric-array": + expectTypeOf(value.value).toEqualTypeOf(); + expectTypeOf(value.fieldName).toEqualTypeOf<"Numeric Array">(); + break; + default: + break; + } + }); + } + }); + }); + + describe("pubType", () => { + it("test pubType Get return ", async () => { + type PubTypeGetReturn = Awaited>; + + const pubType = {} as unknown as PubTypeGetReturn; + + if (pubType.status !== 200) { + expectTypeOf(pubType.body).toEqualTypeOf(); + return; + } + + expectTypeOf(pubType.body.name).toMatchTypeOf<(typeof pubTypeNames)[number]>(); + + if (pubType.body.name === "Basic Pub") { + pubType.body.fields.map((field) => { + expectTypeOf(field.name).toEqualTypeOf<"Title" | "Some relation">(); + }); + } + }); + }); +}); diff --git a/packages/platform-sdk/src/type-generator.ts b/packages/platform-sdk/src/type-generator.ts new file mode 100644 index 0000000000..0cde987204 --- /dev/null +++ b/packages/platform-sdk/src/type-generator.ts @@ -0,0 +1,306 @@ +import fs from "fs/promises"; +import path from "path"; + +import type { PubTypeWithFields } from "contracts"; +import type { + CommunitiesId, + PubFields, + PubFieldSchemaId, + PubFieldsId, + PubTypes, + PubTypesId, +} from "db/public"; +import { CoreSchemaType } from "db/public"; + +import type { Prettify } from "./types"; + +type SchemaNameToTypeMap = { + [CoreSchemaType.Boolean]: boolean; + [CoreSchemaType.DateTime]: Date; + [CoreSchemaType.Email]: string; + [CoreSchemaType.MemberId]: string; + [CoreSchemaType.Number]: number; + [CoreSchemaType.NumericArray]: number[]; + [CoreSchemaType.RichText]: { type: "doc"; content: unknown[] }; + [CoreSchemaType.String]: string; + [CoreSchemaType.StringArray]: string[]; + [CoreSchemaType.Vector3]: [number, number, number]; + [CoreSchemaType.URL]: string; + [CoreSchemaType.Null]: null; + [CoreSchemaType.FileUpload]: { + id: string; + name: string; + type: string; + size: number; + url: string; + }; +}; + +type SchemaNameToType = SchemaNameToTypeMap[T]; + +const schemaNameToSortOfType = { + [CoreSchemaType.Boolean]: true as boolean, + [CoreSchemaType.DateTime]: new Date() as Date, + [CoreSchemaType.Email]: "" as string, + [CoreSchemaType.MemberId]: "" as string, + [CoreSchemaType.Number]: 1 as number, + [CoreSchemaType.NumericArray]: [1, 2, 3] as number[], + [CoreSchemaType.RichText]: { type: "doc", content: [] } as { type: "doc"; content: unknown[] }, + [CoreSchemaType.String]: "" as string, + [CoreSchemaType.StringArray]: ["hey"] as string[], + [CoreSchemaType.Vector3]: [1, 2, 3] as [number, number, number], + [CoreSchemaType.URL]: "hey" as string, + [CoreSchemaType.Null]: null, + [CoreSchemaType.FileUpload]: {} as { + id: string; + name: string; + type: string; + size: number; + url: string; + }, +} as const satisfies Record; + +const schemaNameToType = { + [CoreSchemaType.Boolean]: "boolean", + [CoreSchemaType.DateTime]: "Date", + [CoreSchemaType.Email]: "string", + [CoreSchemaType.MemberId]: "string", + [CoreSchemaType.Number]: "number", + [CoreSchemaType.NumericArray]: "number[]", + [CoreSchemaType.RichText]: "{ type: 'doc'; content: unknown[] }", + [CoreSchemaType.String]: "string", + [CoreSchemaType.StringArray]: "string[]", + [CoreSchemaType.Vector3]: "[number, number, number]", + [CoreSchemaType.URL]: "string", + [CoreSchemaType.Null]: "null", + [CoreSchemaType.FileUpload]: "{}", +} as const satisfies Record; + +const toSafeTypeName = (name: string) => { + return name.replace(/[^a-zA-Z0-9]/g, "_"); +}; + +const slugToSafeTypeName = (slug: string) => { + const slugParts = slug.split(":"); + + const candidateSlug = slugParts[slugParts.length - 1] ?? slugParts[0]; + + const typeName = toSafeTypeName(candidateSlug); + + if (!typeName) { + throw new Error(`Invalid slug: ${slug}`); + } + return typeName; +}; + +const createSchemaNameToTypeMap = () => { + return `export type SchemaNameToTypeMap = { + [CoreSchemaType.Boolean]: boolean; + [CoreSchemaType.DateTime]: Date; + [CoreSchemaType.Email]: string; + [CoreSchemaType.MemberId]: string; + [CoreSchemaType.Number]: number; + [CoreSchemaType.NumericArray]: number[]; + [CoreSchemaType.RichText]: { type: "doc"; content: unknown[] }; + [CoreSchemaType.String]: string; + [CoreSchemaType.StringArray]: string[]; + [CoreSchemaType.Vector3]: [number, number, number]; + [CoreSchemaType.URL]: string; + [CoreSchemaType.Null]: null; + [CoreSchemaType.FileUpload]: { + id: string; + name: string; + type: string; + size: number; + url: string; + }; +}`; +}; + +const createPubFieldTypes = (pubTypes: PubTypeWithFields[]) => { + const allFieldsFromPubTypes = pubTypes.flatMap((pubType) => Object.values(pubType.fields)); + + // get unique fields + + const uniqueFields = allFieldsFromPubTypes.filter( + (field, index, self) => index === self.findIndex((t) => t.slug === field.slug) + ); + + const fieldTypes = uniqueFields.map((field) => { + return `export type PubField_${slugToSafeTypeName(field.slug)} = { + id: string; + name: "${field.name}"; + slug: "${field.slug}"; + schemaName: CoreSchemaType.${field.schemaName}; + isRelation: ${field.isRelation}; + isTitle: ${field.isTitle}; +}`; + }); + + return `${fieldTypes.join("\n\n")}; + +export type PubField = ${uniqueFields.map((field) => `PubField_${slugToSafeTypeName(field.slug)}`).join(" | ")};`; +}; + +const createPubTypeTypes = (pubTypes: PubTypeWithFields[]) => { + const pubTypeTypeNames = pubTypes.map( + (pubType) => `PubType_${slugToSafeTypeName(pubType.name)}` + ); + + const pubTypeTypes = pubTypes.map((pubType) => { + return `export type PubType_${slugToSafeTypeName(pubType.name)} = { + id: PubTypesId; + createdAt: Date; + updatedAt: Date; + communityId: CommunitiesId; + name: "${pubType.name}"; + description: ${pubType.description ? `"${pubType.description}"` : "null"}; + fields: (${pubType.fields.map((field) => `PubField_${slugToSafeTypeName(field.slug)}`).join(" | ")})[]; + }`; + }); + + return `${pubTypeTypes.join("\n\n")}; + +export type PubType = ${pubTypeTypeNames.join(" | ")}`; +}; + +const createPubFieldTypeToValueType = () => { + return `export type PubFieldToValueType = Prettify<{ + id: PubFieldsId; + createdAt: Date; + updatedAt: Date; + fieldSlug: T["slug"]; + fieldName: T["name"]; + schemaName: T["schemaName"]; + value: SchemaNameToTypeMap[T["schemaName"]]; +} & (T["isRelation"] extends true ? { + relatedPubId: PubTypesId | null; + relatedPub: Pub | null; + } : { + relatedPubId: null; + relatedPub?: never; +})>`; +}; + +const createPubTypes = (pubTypes: PubTypeWithFields[]) => { + const pubs = pubTypes.map((pubType) => { + return `export type Pub_${slugToSafeTypeName(pubType.name)} = { + id: PubsId; + title: string; + parentId: PubsId; + depth: number; + pubTypeId: PubTypesId; + stageId: StagesId; + communityId: CommunitiesId; + createdAt: Date; + updatedAt: Date; + pubTypeName: "${pubType.name}"; + pubType: PubType_${slugToSafeTypeName(pubType.name)}; + values: (${pubType.fields + .map((field) => `PubFieldToValueType`) + .join(" | ")})[] +}`; + }); + + return `${pubs.join("\n\n")} + +export type Pub = ${pubTypes.map((pubType) => `Pub_${slugToSafeTypeName(pubType.name)}`).join(" | ")} +`; +}; + +export async function generateTypeDefinitions(pubTypes: PubTypeWithFields[], outputPath: string) { + // Generate the type definition file content + const typeContent = `// Generated by PubPub Type Generator +import type { CoreSchemaType, PubFieldsId, PubTypesId, CommunitiesId, StagesId, PubsId } from "db/public"; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +export interface PubTypeField { + id: PubFieldsId; + name: string; + slug: string; + schemaName: CoreSchemaType; + isRelation: boolean; + isTitle: boolean; +} + +export interface PubTypeDefinition { + createdAt: Date; + updatedAt: Date; + id: PubTypesId; + communityId: CommunitiesId; + name: string; + description: string | null; + fields: PubTypeField[]; +} + +${createSchemaNameToTypeMap()} + +${createPubFieldTypes(pubTypes)} + +${createPubTypeTypes(pubTypes)} + +${createPubFieldTypeToValueType()} + +${createPubTypes(pubTypes)} + +export interface CommunitySchema { + pubTypes: { + ${pubTypes + .map( + (pubType) => `"${pubType.name}": { + id: "${pubType.id}", + name: "${pubType.name}", + description: ${pubType.description ? `"${pubType.description}"` : "null"}, + fields: { + ${pubType.fields + .map( + (field) => `"${field.slug}": { + id: "${field.id}", + name: "${field.name}", + slug: "${field.slug}", + schemaName: CoreSchemaType.${field.schemaName}, + isRelation: ${field.isRelation}, + isTitle: ${field.isTitle} + }` + ) + .join(",\n ")} + } + }` + ) + .join(",\n ")} + } +} +`; + // Ensure the output directory exists + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + + // Write the type definition file + await fs.writeFile(outputPath, typeContent); + + return typeContent; +} + +// Infer the values type for a specific PubType +// type InferPubTypeValues> = Prettify<{ +// [K in keyof T["fields"]]: { +// id: T["fields"][K]["id"]; +// fieldName: T["fields"][K]["name"]; +// fieldSlug: T["fields"][K]["slug"]; +// schemaName: T["fields"][K]["schemaName"]; +// value: T["fields"][K]["schemaName"]; +// relatedPubId?: string | null; +// relatedPub?: T["fields"][K]["isRelation"] extends true ? ParentPub : null; +// }; +// }>; + +// export type Pub = T extends PubTypeDefinition +// ? { +// id: string; +// pubTypeName: T["name"]; +// pubType: T; +// values: InferPubTypeValues[keyof T["fields"]]; +// } +// : never; diff --git a/packages/platform-sdk/src/types.ts b/packages/platform-sdk/src/types.ts new file mode 100644 index 0000000000..259e8fb3e7 --- /dev/null +++ b/packages/platform-sdk/src/types.ts @@ -0,0 +1,3 @@ +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; diff --git a/packages/platform-sdk/tsconfig.json b/packages/platform-sdk/tsconfig.json new file mode 100644 index 0000000000..c4346c9039 --- /dev/null +++ b/packages/platform-sdk/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "noEmit": true, + "jsx": "react", + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/platform-sdk/vitest.config.mts b/packages/platform-sdk/vitest.config.mts new file mode 100644 index 0000000000..9cac0d881b --- /dev/null +++ b/packages/platform-sdk/vitest.config.mts @@ -0,0 +1,17 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + exclude: [ + "**/playwright/**", + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + ], + environment: "node", + }, +}); diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 4f0f46c92d..add7bc75ca 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -1,4 +1,4 @@ -import type { Static } from "@sinclair/typebox"; +import type { Static, TSchema } from "@sinclair/typebox"; import { CoreSchemaType } from "db/public"; @@ -19,41 +19,32 @@ import { Vector3, } from "./schemas"; -export function getJsonSchemaByCoreSchemaType(coreSchemaType: CoreSchemaType, config?: unknown) { - switch (coreSchemaType) { - case CoreSchemaType.Boolean: - return Boolean; - case CoreSchemaType.DateTime: - return DateTime; - case CoreSchemaType.Email: - return Email; - case CoreSchemaType.FileUpload: - return FileUpload; - case CoreSchemaType.MemberId: - return MemberId; - case CoreSchemaType.Null: - return Null; - case CoreSchemaType.Number: - return Number; - case CoreSchemaType.NumericArray: - return getNumericArrayWithMinMax(config); - case CoreSchemaType.RichText: - return RichText; - case CoreSchemaType.String: - return String; - case CoreSchemaType.StringArray: - return getStringArrayWithMinMax(config); - case CoreSchemaType.URL: - return URL; - case CoreSchemaType.Vector3: - return Vector3; - default: - const _exhaustiveCheck: never = coreSchemaType; - return _exhaustiveCheck; - } -} +const SCHEMA_TYPE_SCHEMA_MAP = { + [CoreSchemaType.Boolean]: (config: unknown) => Boolean, + [CoreSchemaType.DateTime]: (config: unknown) => DateTime, + [CoreSchemaType.Email]: (config: unknown) => Email, + [CoreSchemaType.FileUpload]: (config: unknown) => FileUpload, + [CoreSchemaType.MemberId]: (config: unknown) => MemberId, + [CoreSchemaType.Null]: (config: unknown) => Null, + [CoreSchemaType.Number]: (config: unknown) => Number, + [CoreSchemaType.NumericArray]: getNumericArrayWithMinMax, + [CoreSchemaType.RichText]: (config: unknown) => RichText, + [CoreSchemaType.String]: (config: unknown) => String, + [CoreSchemaType.StringArray]: getStringArrayWithMinMax, + [CoreSchemaType.URL]: (config: unknown) => URL, + [CoreSchemaType.Vector3]: (config: unknown) => Vector3, +} as const satisfies Record TSchema>; -export type JSONSchemaForCoreSchemaType = (typeof Schemas)[C]; +export type JSONSchemaForCoreSchemaType = ReturnType< + (typeof SCHEMA_TYPE_SCHEMA_MAP)[C] +>; + +export function getJsonSchemaByCoreSchemaType( + coreSchemaType: T, + config?: unknown +) { + return SCHEMA_TYPE_SCHEMA_MAP[coreSchemaType](config) as JSONSchemaForCoreSchemaType; +} export type InputTypeForCoreSchemaType = Static<(typeof Schemas)[C]>; diff --git a/packages/schemas/src/schemas.ts b/packages/schemas/src/schemas.ts index ed8a062d49..9185ba092b 100644 --- a/packages/schemas/src/schemas.ts +++ b/packages/schemas/src/schemas.ts @@ -167,6 +167,12 @@ export const FileUpload = Type.Array( format: "uri", description: "The URL to upload the file to.", }), + filePreview: Type.Optional( + Type.String({ + format: "uri", + description: "The URL to the preview image of the file.", + }) + ), }, { description: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06ac29c37c..538c7130c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -595,8 +595,8 @@ importers: specifier: ^5.0.1 version: 5.0.1(typescript@5.6.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1)) vitest: - specifier: ^2.1.1 - version: 2.1.1(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.34.1) + specifier: ^2.1.8 + version: 2.1.8(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.34.1) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -1123,6 +1123,40 @@ importers: specifier: 'catalog:' version: 5.6.2 + packages/platform-sdk: + dependencies: + '@ts-rest/core': + specifier: 'catalog:' + version: 3.51.0(@types/node@20.16.5)(zod@3.23.8) + contracts: + specifier: workspace:* + version: link:../contracts + db: + specifier: workspace:* + version: link:../db + utils: + specifier: workspace:* + version: link:../utils + devDependencies: + '@pubpub/prettier-config': + specifier: workspace:* + version: link:../../config/prettier + '@types/node': + specifier: 'catalog:' + version: 20.16.5 + tsconfig: + specifier: workspace:* + version: link:../../config/tsconfig + typescript: + specifier: 'catalog:' + version: 5.6.2 + vite-tsconfig-paths: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.6.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1)) + vitest: + specifier: ^2.1.8 + version: 2.1.8(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.34.1) + packages/schemas: dependencies: '@sinclair/typebox': @@ -6783,14 +6817,13 @@ packages: '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/expect@2.1.1': - resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + '@vitest/expect@2.1.8': + resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} - '@vitest/mocker@2.1.1': - resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + '@vitest/mocker@2.1.8': + resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} peerDependencies: - '@vitest/spy': 2.1.1 - msw: ^2.3.5 + msw: ^2.4.9 vite: ^5.0.0 peerDependenciesMeta: msw: @@ -6804,17 +6837,20 @@ packages: '@vitest/pretty-format@2.1.1': resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} + '@vitest/pretty-format@2.1.8': + resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + '@vitest/runner@1.6.0': resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} - '@vitest/runner@2.1.1': - resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + '@vitest/runner@2.1.8': + resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} '@vitest/snapshot@1.6.0': resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} - '@vitest/snapshot@2.1.1': - resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} + '@vitest/snapshot@2.1.8': + resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} '@vitest/spy@1.6.0': resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} @@ -6822,8 +6858,8 @@ packages: '@vitest/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/spy@2.1.1': - resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} + '@vitest/spy@2.1.8': + resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} @@ -6834,6 +6870,9 @@ packages: '@vitest/utils@2.1.1': resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + '@vitest/utils@2.1.8': + resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -7310,6 +7349,10 @@ packages: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} engines: {node: '>=12'} + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chainsaw@0.0.9: resolution: {integrity: sha512-nG8PYH+/4xB+8zkV4G844EtfvZ5tTiLFoX3dZ4nhF4t3OCKIb9UvaFyNmeZO2zOSmRWzBoTD+napN6hiL+EgcA==} @@ -8237,6 +8280,10 @@ packages: exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + express@4.21.0: resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==} engines: {node: '>= 0.10.0'} @@ -9291,6 +9338,7 @@ packages: resolution: {integrity: sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw==} engines: {node: '>=8.17.0'} hasBin: true + bundledDependencies: [] jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -9555,6 +9603,9 @@ packages: loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -9605,6 +9656,9 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -11403,6 +11457,9 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -11663,8 +11720,8 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.0: - resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} @@ -12155,8 +12212,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite-node@2.1.1: - resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} + vite-node@2.1.8: + resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -12224,15 +12281,15 @@ packages: jsdom: optional: true - vitest@2.1.1: - resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} + vitest@2.1.8: + resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.1 - '@vitest/ui': 2.1.1 + '@vitest/browser': 2.1.8 + '@vitest/ui': 2.1.8 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -19363,18 +19420,18 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/expect@2.1.1': + '@vitest/expect@2.1.8': dependencies: - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 - chai: 5.1.1 + '@vitest/spy': 2.1.8 + '@vitest/utils': 2.1.8 + chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1))': + '@vitest/mocker@2.1.8(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1))': dependencies: - '@vitest/spy': 2.1.1 + '@vitest/spy': 2.1.8 estree-walker: 3.0.3 - magic-string: 0.30.11 + magic-string: 0.30.17 optionalDependencies: vite: 5.4.3(@types/node@20.16.5)(terser@5.34.1) @@ -19386,15 +19443,19 @@ snapshots: dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@2.1.8': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/runner@1.6.0': dependencies: '@vitest/utils': 1.6.0 p-limit: 5.0.0 pathe: 1.1.2 - '@vitest/runner@2.1.1': + '@vitest/runner@2.1.8': dependencies: - '@vitest/utils': 2.1.1 + '@vitest/utils': 2.1.8 pathe: 1.1.2 '@vitest/snapshot@1.6.0': @@ -19403,10 +19464,10 @@ snapshots: pathe: 1.1.2 pretty-format: 29.7.0 - '@vitest/snapshot@2.1.1': + '@vitest/snapshot@2.1.8': dependencies: - '@vitest/pretty-format': 2.1.1 - magic-string: 0.30.11 + '@vitest/pretty-format': 2.1.8 + magic-string: 0.30.17 pathe: 1.1.2 '@vitest/spy@1.6.0': @@ -19417,7 +19478,7 @@ snapshots: dependencies: tinyspy: 3.0.2 - '@vitest/spy@2.1.1': + '@vitest/spy@2.1.8': dependencies: tinyspy: 3.0.2 @@ -19441,6 +19502,12 @@ snapshots: loupe: 3.1.1 tinyrainbow: 1.2.0 + '@vitest/utils@2.1.8': + dependencies: + '@vitest/pretty-format': 2.1.8 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -20000,6 +20067,14 @@ snapshots: loupe: 3.1.1 pathval: 2.0.0 + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chainsaw@0.0.9: dependencies: traverse: 0.3.9 @@ -21117,6 +21192,8 @@ snapshots: exifr@7.1.3: {} + expect-type@1.1.0: {} + express@4.21.0: dependencies: accepts: 1.3.8 @@ -22631,6 +22708,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.2: {} + lower-case-first@1.0.2: dependencies: lower-case: 1.1.4 @@ -22678,6 +22757,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -24934,6 +25017,8 @@ snapshots: std-env@3.7.0: {} + std-env@3.8.0: {} + stop-iteration-iterator@1.0.0: dependencies: internal-slot: 1.0.7 @@ -25263,7 +25348,7 @@ snapshots: tinycolor2@1.6.0: {} - tinyexec@0.3.0: {} + tinyexec@0.3.2: {} tinygradient@1.1.5: dependencies: @@ -25756,10 +25841,11 @@ snapshots: - supports-color - terser - vite-node@2.1.1(@types/node@20.16.5)(terser@5.34.1): + vite-node@2.1.8(@types/node@20.16.5)(terser@5.34.1): dependencies: cac: 6.7.14 debug: 4.3.7 + es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 5.4.3(@types/node@20.16.5)(terser@5.34.1) transitivePeerDependencies: @@ -25839,26 +25925,27 @@ snapshots: - supports-color - terser - vitest@2.1.1(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.34.1): + vitest@2.1.8(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.34.1): dependencies: - '@vitest/expect': 2.1.1 - '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1)) - '@vitest/pretty-format': 2.1.1 - '@vitest/runner': 2.1.1 - '@vitest/snapshot': 2.1.1 - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 - chai: 5.1.1 + '@vitest/expect': 2.1.8 + '@vitest/mocker': 2.1.8(vite@5.4.3(@types/node@20.16.5)(terser@5.34.1)) + '@vitest/pretty-format': 2.1.8 + '@vitest/runner': 2.1.8 + '@vitest/snapshot': 2.1.8 + '@vitest/spy': 2.1.8 + '@vitest/utils': 2.1.8 + chai: 5.1.2 debug: 4.3.7 - magic-string: 0.30.11 + expect-type: 1.1.0 + magic-string: 0.30.17 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.8.0 tinybench: 2.9.0 - tinyexec: 0.3.0 + tinyexec: 0.3.2 tinypool: 1.0.1 tinyrainbow: 1.2.0 vite: 5.4.3(@types/node@20.16.5)(terser@5.34.1) - vite-node: 2.1.1(@types/node@20.16.5)(terser@5.34.1) + vite-node: 2.1.8(@types/node@20.16.5)(terser@5.34.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.16.5 From c7df430341b81309e3d746d6abd16af504e1bc7f Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Jan 2025 15:14:34 +0100 Subject: [PATCH 02/10] fix: add zod dep --- packages/platform-sdk/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/platform-sdk/package.json b/packages/platform-sdk/package.json index 6ed6c9021f..94fef76d1f 100644 --- a/packages/platform-sdk/package.json +++ b/packages/platform-sdk/package.json @@ -25,7 +25,8 @@ "@ts-rest/core": "catalog:", "contracts": "workspace:*", "db": "workspace:*", - "utils": "workspace:*" + "utils": "workspace:*", + "zod": "catalog:" }, "devDependencies": { "@pubpub/prettier-config": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 538c7130c3..3edfcfba84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1137,6 +1137,9 @@ importers: utils: specifier: workspace:* version: link:../utils + zod: + specifier: 'catalog:' + version: 3.23.8 devDependencies: '@pubpub/prettier-config': specifier: workspace:* From b5b7bf972d898768a1711b0b7b8d434d5d927926 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Jan 2025 15:21:41 +0100 Subject: [PATCH 03/10] fix: add prettier ignore --- packages/platform-sdk/.prettierignore | 2 ++ packages/platform-sdk/package.json | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/platform-sdk/.prettierignore diff --git a/packages/platform-sdk/.prettierignore b/packages/platform-sdk/.prettierignore new file mode 100644 index 0000000000..70bf9c5adf --- /dev/null +++ b/packages/platform-sdk/.prettierignore @@ -0,0 +1,2 @@ +**/.generated-pubtypes.ts +**/__snapshots__ \ No newline at end of file diff --git a/packages/platform-sdk/package.json b/packages/platform-sdk/package.json index 94fef76d1f..2ce8d2f0f7 100644 --- a/packages/platform-sdk/package.json +++ b/packages/platform-sdk/package.json @@ -9,8 +9,8 @@ "dist" ], "scripts": { - "format": "prettier --check . --ignore-path ../../.gitignore", - "format:fix": "prettier -w . --ignore-path ../../.gitignore", + "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore", + "format:fix": "prettier -w . --ignore-path ../../.gitignore --ignore-path .prettierignore", "type-check": "tsc --noEmit", "test": "vitest" }, From aea0910ccebc8e7d632e2c1011d07b4ae01f852c Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Jan 2025 15:23:52 +0100 Subject: [PATCH 04/10] chore: add eslint --- packages/platform-sdk/eslint.config.mjs | 11 +++++++++++ packages/platform-sdk/package.json | 3 +++ pnpm-lock.yaml | 3 +++ 3 files changed, 17 insertions(+) create mode 100644 packages/platform-sdk/eslint.config.mjs diff --git a/packages/platform-sdk/eslint.config.mjs b/packages/platform-sdk/eslint.config.mjs new file mode 100644 index 0000000000..8838016413 --- /dev/null +++ b/packages/platform-sdk/eslint.config.mjs @@ -0,0 +1,11 @@ +// @ts-check + +import baseConfig from "@pubpub/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: ["dist/**", "**/.generated-pubtypes.ts", "**/__snapshots__/**"], + }, + ...baseConfig, +]; diff --git a/packages/platform-sdk/package.json b/packages/platform-sdk/package.json index 2ce8d2f0f7..91bac7c837 100644 --- a/packages/platform-sdk/package.json +++ b/packages/platform-sdk/package.json @@ -11,6 +11,8 @@ "scripts": { "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore", "format:fix": "prettier -w . --ignore-path ../../.gitignore --ignore-path .prettierignore", + "lint": "eslint **/*.ts", + "lint:fix": "eslint --fix **/*.ts", "type-check": "tsc --noEmit", "test": "vitest" }, @@ -30,6 +32,7 @@ }, "devDependencies": { "@pubpub/prettier-config": "workspace:*", + "@pubpub/eslint-config": "workspace:*", "@types/node": "catalog:", "tsconfig": "workspace:*", "typescript": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3edfcfba84..ee7c95fa43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1141,6 +1141,9 @@ importers: specifier: 'catalog:' version: 3.23.8 devDependencies: + '@pubpub/eslint-config': + specifier: workspace:* + version: link:../../config/eslint '@pubpub/prettier-config': specifier: workspace:* version: link:../../config/prettier From a92383eeac181ec7dc4acc16dbd49336bed31a08 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 9 Jan 2025 15:33:54 +0100 Subject: [PATCH 05/10] fix: modify tsconfig.json to work --- packages/platform-sdk/tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/platform-sdk/tsconfig.json b/packages/platform-sdk/tsconfig.json index c4346c9039..f5fe13030f 100644 --- a/packages/platform-sdk/tsconfig.json +++ b/packages/platform-sdk/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "tsconfig/react-library.json", + "extends": "tsconfig/base.json", "compilerOptions": { "noEmit": true, - "jsx": "react", + "module": "ESNext", "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, - "include": ["."], + "include": ["./src/**/*.ts"], "exclude": ["dist", "build", "node_modules"] } From aaa685b493b6dce8f4da29791b3115215e98ac18 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 16 Jan 2025 16:54:43 +0100 Subject: [PATCH 06/10] fix: update platform-sdk to esm --- packages/platform-sdk/package.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/platform-sdk/package.json b/packages/platform-sdk/package.json index 91bac7c837..8cee8084a8 100644 --- a/packages/platform-sdk/package.json +++ b/packages/platform-sdk/package.json @@ -3,8 +3,11 @@ "private": true, "version": "0.0.1", "description": "SDK for the PubPub platform", - "main": "dist/pubpub-platform-sdk.cjs.js", - "module": "dist/pubpub-platform-sdk.esm.js", + "exports": { + ".": "./dist/pubpub-platform-sdk.js", + "./package.json": "./package.json" + }, + "type": "module", "files": [ "dist" ], @@ -40,6 +43,12 @@ "vitest": "^2.1.8" }, "preconstruct": { + "exports": true, + "___experimentalFlags_WILL_CHANGE_IN_PATCH": { + "typeModule": true, + "distInRoot": true, + "importsConditions": true + }, "entrypoints": [ "index.ts" ] From f748407c44b08e94fa3f05ab754148f86fcd8247 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 16 Jan 2025 17:08:12 +0100 Subject: [PATCH 07/10] fix: set module resolution bundler >:( --- packages/platform-sdk/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/platform-sdk/tsconfig.json b/packages/platform-sdk/tsconfig.json index f5fe13030f..4d1fa30aad 100644 --- a/packages/platform-sdk/tsconfig.json +++ b/packages/platform-sdk/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "noEmit": true, "module": "ESNext", + "moduleResolution": "bundler", "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, "include": ["./src/**/*.ts"], From a42163693ae58d8d9a0697e3565b0b92485abdb3 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 16 Jan 2025 17:09:00 +0100 Subject: [PATCH 08/10] fix: remove integrationId --- packages/contracts/src/resources/site.ts | 44 +++++++++--------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/contracts/src/resources/site.ts b/packages/contracts/src/resources/site.ts index beb148d9ac..2906ab2281 100644 --- a/packages/contracts/src/resources/site.ts +++ b/packages/contracts/src/resources/site.ts @@ -102,27 +102,21 @@ const upsertPubRelationsSchema = z.record( ) ); -/** - * Only add the `children` if the `withChildren` option has not been set to `false - */ +/** Only add the `children` if the `withChildren` option has not been set to `false */ type MaybePubChildren = Options["withChildren"] extends false ? { children?: never } : Options["withChildren"] extends undefined ? { children?: ProcessedPub[] } : { children: ProcessedPub[] }; -/** - * Only add the `stage` if the `withStage` option has not been set to `false - */ +/** Only add the `stage` if the `withStage` option has not been set to `false */ type MaybePubStage = Options["withStage"] extends true ? { stage: Stages | null } : Options["withStage"] extends false ? { stage?: never } : { stage?: Stages | null }; -/** - * Only add the `pubType` if the `withPubType` option has not been set to `false - */ +/** Only add the `pubType` if the `withPubType` option has not been set to `false */ export type PubTypePubField = Pick< PubFields, "id" | "name" | "slug" | "schemaName" | "isRelation" @@ -139,9 +133,7 @@ type MaybePubPubType = Options["withPubType"] e ? { pubType?: never } : { pubType?: PubTypes & { fields: PubTypePubField[] } }; -/** - * Only add the `pubType` if the `withPubType` option has not been set to `false - */ +/** Only add the `pubType` if the `withPubType` option has not been set to `false */ type MaybePubMembers = Options["withMembers"] extends true ? { members: (Omit & { role: MemberRole })[] } : Options["withMembers"] extends false @@ -160,17 +152,17 @@ type MaybePubLegacyAssignee = : { assignee?: Users | null }; /** - * Those options of `GetPubsWithRelatedValuesAndChildrenOptions` that affect the output of `ProcessedPub` + * Those options of `GetPubsWithRelatedValuesAndChildrenOptions` that affect the output of + * `ProcessedPub` * - * This way it's more easy to specify what kind of `ProcessedPub` we want as e.g. the input type of a function - * - **/ + * This way it's more easy to specify what kind of `ProcessedPub` we want as e.g. the input type of + * a function + */ export type MaybePubOptions = { /** * Whether to recursively fetch children up to depth `depth`. * * @default true - * */ withChildren?: boolean; /** @@ -217,9 +209,7 @@ type ValueBase = { value: unknown; createdAt: Date; updatedAt: Date; - /** - * Information about the field that the value belongs to. - */ + /** Information about the field that the value belongs to. */ schemaName: CoreSchemaType; fieldSlug: string; fieldName: string; @@ -236,19 +226,18 @@ type ProcessedPubBase = { depth: number; isCycle?: boolean; /** - * The `updatedAt` of the latest value, or of the pub if the pub itself has a higher `updatedAt` or if there are no values + * The `updatedAt` of the latest value, or of the pub if the pub itself has a higher `updatedAt` + * or if there are no values * - * We do this because the Pub itself is rarely if ever changed over time. - * TODO: Possibly add the `updatedAt` of `PubsInStages` here as well? - * At time of writing (2024/11/04) I don't think that table has an `updatedAt`. + * We do this because the Pub itself is rarely if ever changed over time. TODO: Possibly add the + * `updatedAt` of `PubsInStages` here as well? At time of writing (2024/11/04) I don't think + * that table has an `updatedAt`. */ updatedAt: Date; }; export type ProcessedPub = ProcessedPubBase & { - /** - * Is an empty array if `withValues` is false - */ + /** Is an empty array if `withValues` is false */ values: (ValueBase & MaybePubRelatedPub)[]; } & MaybePubChildren & MaybePubStage & @@ -284,7 +273,6 @@ const pubTypeReturnSchema = pubTypesSchema.extend({ updatedAt: true, communityId: true, isArchived: true, - integrationId: true, pubFieldSchemaId: true, }) .extend({ From df89951c323c9090e89a37d7d0173b81761710cb Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 16 Jan 2025 17:11:21 +0100 Subject: [PATCH 09/10] fix: remove pubtype error --- core/lib/server/pubtype.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/server/pubtype.ts b/core/lib/server/pubtype.ts index e199bdc4f4..32055e07fe 100644 --- a/core/lib/server/pubtype.ts +++ b/core/lib/server/pubtype.ts @@ -3,7 +3,7 @@ import type { ExpressionBuilder } from "kysely"; import { sql } from "kysely"; import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/postgres"; -import type { PubTypeWithFields } from "contracts/src/resources/site"; +import type { PubTypeWithFields } from "contracts"; import type { CommunitiesId, FormsId, PubFieldsId, PubsId, PubTypesId } from "db/public"; import type { AutoReturnType, Equal, Expect, Prettify, XOR } from "../types"; From 675c0ed506b94b3accbdf2e79d23df0e80269a47 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Wed, 12 Mar 2025 15:14:11 +0100 Subject: [PATCH 10/10] chore: merge --- .env.docker-compose.dev | 13 + .env.docker-compose.test | 40 + .github/dependabot.yml | 55 + .github/workflows/build-docs.yml | 80 + .github/workflows/ci.yml | 12 +- .github/workflows/e2e.yml | 51 +- .github/workflows/ecrbuild-all.yml | 21 + .github/workflows/ecrbuild-template.yml | 37 +- .github/workflows/on_main.yml | 11 + .github/workflows/on_pr.yml | 96 +- .github/workflows/pull-preview.yml | 77 + .gitignore | 4 +- .nvmrc | 2 +- .vscode/extensions.json | 4 +- Dockerfile | 5 +- README.md | 6 +- config/prettier/index.js | 2 +- core/.env.development | 7 +- core/.env.test | 13 +- core/.github/workflows/playwright.yml | 2 +- core/README.md | 16 +- .../actions/_lib/runActionInstance.db.test.ts | 2 +- core/actions/_lib/runActionInstance.ts | 4 +- core/actions/datacite/action.ts | 12 +- core/actions/datacite/run.test.ts | 3 +- core/actions/datacite/run.ts | 10 +- core/actions/email/action.ts | 35 +- ...nt.field.tsx => recipientMember.field.tsx} | 15 +- ...nt.field.tsx => recipientMember.field.tsx} | 15 +- core/actions/email/run.ts | 95 +- .../actions/googleDriveImport/OutputField.tsx | 3 +- .../config/outputField.field.tsx | 2 +- .../googleDriveImport/discussionSchema.ts | 446 + .../googleDriveImport/formatDriveData.ts | 149 +- .../googleDriveImport/gdocPlugins.test.ts | 758 +- core/actions/googleDriveImport/gdocPlugins.ts | 652 +- .../googleDriveImport/getGDriveFiles.ts | 114 +- .../params/outputField.field.tsx | 2 +- core/actions/googleDriveImport/run.ts | 127 +- core/actions/http/run.ts | 2 +- core/actions/move/run.ts | 22 +- core/app/(user)/forgot/ForgotForm.tsx | 4 + core/app/(user)/login/LoginForm.tsx | 7 +- core/app/(user)/login/page.tsx | 7 +- core/app/(user)/reset/ResetForm.tsx | 12 +- .../site/[...ts-rest]/route.ts | 78 +- .../[formSlug]/fill/ExternalFormWrapper.tsx | 4 +- .../forms/[formSlug]/fill/RequestLink.tsx | 2 +- .../public/forms/[formSlug]/fill/page.tsx | 12 +- .../c/[communitySlug]/CommunitySwitcher.tsx | 27 +- core/app/c/[communitySlug]/ContentLayout.tsx | 7 +- core/app/c/[communitySlug]/LoginSwitcher.tsx | 89 +- core/app/c/[communitySlug]/NavLink.module.css | 29 - core/app/c/[communitySlug]/NavLink.tsx | 79 +- core/app/c/[communitySlug]/NavLinkSubMenu.tsx | 92 + core/app/c/[communitySlug]/SideNav.tsx | 480 +- .../activity/actions/ActionRunsTable.tsx | 12 +- .../actions/getActionRunsTableColumns.tsx | 23 +- .../[communitySlug]/activity/actions/page.tsx | 20 +- core/app/c/[communitySlug]/fields/page.tsx | 3 +- .../forms/[formSlug]/edit/page.tsx | 14 +- core/app/c/[communitySlug]/forms/actions.ts | 4 +- core/app/c/[communitySlug]/forms/page.tsx | 2 +- core/app/c/[communitySlug]/layout.tsx | 38 +- core/app/c/[communitySlug]/pubs/PubList.tsx | 4 +- .../[pubId]/components/PubChildrenTable.tsx | 27 - .../components/PubChildrenTableWrapper.tsx | 141 - .../[pubId]/components/RelatedPubsTable.tsx | 9 +- .../__tests__/article-pub-fixture.json | 2 +- .../__tests__/author-pub-fixture.json | 6 +- .../components/getPubChildrenTableColumns.tsx | 130 - .../pubs/[pubId]/components/queries.ts | 145 - .../pubs/[pubId]/components/types.ts | 41 - .../pubs/[pubId]/edit/page.tsx | 12 +- .../c/[communitySlug]/pubs/[pubId]/page.tsx | 63 +- .../c/[communitySlug]/pubs/create/page.tsx | 14 +- .../settings/tokens/CreateTokenForm.tsx | 15 +- .../settings/tokens/PermissionField.tsx | 27 +- .../stages/components/Assign.tsx | 1 - .../stages/components/AssignWrapper.tsx | 1 - .../stages/components/StageList.tsx | 31 +- .../stages/components/StagePubActions.tsx | 1 - .../stages/components/lib/actions.ts | 13 +- .../components/editor/StageEditorMenubar.tsx | 2 + .../components/panel/StagePanelMembers.tsx | 2 +- .../components/panel/StagePanelPubs.tsx | 4 +- .../actionsTab/StagePanelActionEditor.tsx | 4 +- .../app/c/[communitySlug]/types/TypeBlock.tsx | 2 +- core/app/c/[communitySlug]/types/actions.ts | 8 +- core/app/c/[communitySlug]/types/page.tsx | 1 - .../ActionUI/ActionConfigFormWrapper.tsx | 1 - .../ContextEditor/ContextEditorClient.tsx | 7 +- core/app/components/DataTable/DataTable.tsx | 27 +- .../app/components/DataTable/v2/DataTable.tsx | 15 +- .../ElementPanel/ButtonConfigurationForm.tsx | 15 +- .../ComponentConfig/RelationBlock.tsx | 47 + .../ElementPanel/ComponentConfig/index.tsx | 10 +- .../InputComponentConfigurationForm.tsx | 111 +- .../FormBuilder/ElementPanel/SelectAccess.tsx | 4 +- .../ElementPanel/SelectElement.tsx | 34 +- .../StructuralElementConfigurationForm.tsx | 2 +- .../FormBuilder/ElementPanel/index.tsx | 2 +- core/app/components/FormBuilder/FieldIcon.tsx | 24 + .../components/FormBuilder/FormBuilder.tsx | 142 +- .../components/FormBuilder/FormElement.tsx | 75 +- .../FormBuilder/SubmissionSettings.tsx | 2 +- core/app/components/FormBuilder/actions.ts | 3 +- core/app/components/FormBuilder/types.ts | 4 +- .../LastVisitedCommunity/SetLastVisited.tsx | 13 + .../LastVisitedCommunity/constants.ts | 2 + core/app/components/LogoutButton.tsx | 44 +- .../MemberSelectAddUserButton.tsx | 20 +- .../MemberSelect/MemberSelectAddUserForm.tsx | 13 +- .../MemberSelect/MemberSelectClient.tsx | 24 +- .../MemberSelect/MemberSelectClientFetch.tsx | 110 + .../MemberSelect/MemberSelectServer.tsx | 81 - .../components/Memberships/MembersList.tsx | 2 +- .../app/components/Memberships/RoleSelect.tsx | 4 +- core/app/components/PubRow.tsx | 83 +- core/app/components/SearchDialog.tsx | 148 + core/app/components/SidePanel.tsx | 67 + .../components/__tests__/PubTitle.test.tsx | 1 - .../components/forms/AddRelatedPubsPanel.tsx | 106 + core/app/components/forms/FileUpload.tsx | 2 +- core/app/components/forms/FormElement.tsx | 157 +- .../forms/FormElementToggleContext.tsx | 11 + .../components/forms/PubFieldFormElement.tsx | 165 + .../forms/elements/CheckboxGroupElement.tsx | 2 +- .../forms/elements/ConfidenceElement.tsx | 4 +- .../forms/elements/ContextEditorElement.tsx | 7 +- .../components/forms/elements/DateElement.tsx | 2 +- .../forms/elements/FileUploadElement.tsx | 6 +- .../forms/elements/MemberSelectElement.tsx | 22 +- .../forms/elements/MultivalueInputElement.tsx | 2 +- .../forms/elements/RadioGroupElement.tsx | 2 +- .../forms/elements/RelatedPubsElement.tsx | 254 + core/app/components/forms/types.ts | 10 +- core/app/components/pubs/CreatePubButton.tsx | 103 +- .../components/pubs/InitialCreatePubForm.tsx | 206 + .../components/pubs/PubEditor/PubEditor.tsx | 144 +- .../pubs/PubEditor/PubEditorClient.tsx | 99 +- .../pubs/PubEditor/PubEditorWrapper.tsx | 3 + .../components/pubs/PubEditor/SaveStatus.tsx | 31 +- core/app/components/pubs/PubEditor/actions.ts | 5 +- .../components/pubs/PubEditor/constants.ts | 1 + core/app/components/pubs/PubEditor/helpers.ts | 3 +- .../app/components/pubs/PubTypeFormClient.tsx | 128 - core/app/components/pubs/RemovePubButton.tsx | 2 +- core/app/components/pubs/RemovePubForm.tsx | 11 +- .../components/pubs/RemovePubFormClient.tsx | 15 +- core/app/layout.tsx | 3 +- core/app/page.tsx | 11 +- core/app/pubs/[pubId]/page.tsx | 27 +- core/docs/pub-relationships.md | 4 +- core/instrumentation.node.mts | 1 - core/kysely/README.md | 92 - core/kysely/database-init.ts | 86 + core/kysely/database.ts | 62 +- core/kysely/scripts/migrate-hierarchy.ts | 140 + core/kysely/updated-at-plugin.test.ts | 170 + core/kysely/updated-at-plugin.ts | 28 +- core/lib/__tests__/db.ts | 26 + core/lib/__tests__/fixtures/big-croc.jpg | Bin 0 -> 6788717 bytes core/{ => lib/__tests__}/globalSetup.ts | 5 +- core/lib/__tests__/live.db.test.ts | 54 +- core/lib/__tests__/matchers.ts | 70 + core/lib/__tests__/pubs.test.ts | 2 - core/lib/__tests__/richText.test.ts | 2 +- core/lib/authentication/README.md | 2 +- core/lib/authentication/actions.ts | 14 +- core/lib/env/env.mjs | 29 +- core/lib/fields/richText.ts | 8 +- core/lib/pubs.ts | 7 +- core/lib/server/assets.db.test.ts | 50 + core/lib/server/assets.ts | 78 +- core/lib/server/cache/README.md | 8 +- core/lib/server/email.tsx | 15 +- core/lib/server/form.ts | 108 +- core/lib/server/mailgun.ts | 86 +- core/lib/server/pub-capabilities.db.test.ts | 24 +- core/lib/server/pub-op.db.test.ts | 1331 +++ core/lib/server/pub-op.ts | 1310 +++ core/lib/server/pub-trigger.db.test.ts | 2 +- core/lib/server/pub.db.test.ts | 515 +- core/lib/server/pub.fts.db.test.ts | 223 + core/lib/server/pub.sort.db.test.ts | 165 + core/lib/server/pub.ts | 925 +- core/lib/server/pubFields.ts | 15 +- core/lib/server/pubtype.ts | 4 +- .../render/pub/renderMarkdownWithPub.ts | 41 +- .../server/render/pub/renderWithPubUtils.ts | 11 +- core/lib/server/stages.ts | 14 +- core/lib/server/user.ts | 51 +- core/lib/server/vitest.d.ts | 14 + core/lib/stages.test.ts | 228 + core/lib/stages.ts | 106 +- core/lib/types.ts | 2 +- core/next.config.mjs | 19 +- core/package.json | 28 +- core/playwright.config.ts | 2 + core/playwright/api/site.spec.ts | 44 + core/playwright/email.spec.ts | 70 + core/playwright/externalFormCreatePub.spec.ts | 179 + core/playwright/externalFormInvite.spec.ts | 81 +- core/playwright/fileUpload.spec.ts | 117 + core/playwright/fixtures/api-token-page.ts | 90 + core/playwright/fixtures/fields-page.ts | 5 +- core/playwright/fixtures/pub-details-page.ts | 26 +- core/playwright/fixtures/pub-types-page.ts | 1 + .../fixtures/test-assets/test-diagram.png | Bin 0 -> 108839 bytes core/playwright/formBuilder.spec.ts | 132 + core/playwright/login.spec.ts | 59 +- core/playwright/pub.spec.ts | 58 +- core/playwright/pubType.spec.ts | 4 +- core/playwright/site-api.spec.ts | 107 + core/prisma/create-admin-user.cts | 94 + core/prisma/db.ts | 27 - core/prisma/exampleCommunitySeeds/arcadia.ts | 39 +- .../exampleCommunitySeeds/arcadiaJournal.ts | 250 + core/prisma/exampleCommunitySeeds/croccroc.ts | 23 +- .../prisma/exampleCommunitySeeds/unjournal.ts | 772 +- .../migration.sql | 2 + .../migration.sql | 103 + .../migration.sql | 29 + .../migration.sql | 19 + .../migration.sql | 92 + .../migration.sql | 8 + core/prisma/schema/schema.dbml | 17 + core/prisma/schema/schema.prisma | 74 +- core/prisma/seed.ts | 20 +- core/prisma/seed/createSeed.ts | 55 + core/prisma/seed/seedCommunity.db.test.ts | 27 +- core/prisma/seed/seedCommunity.ts | 120 +- core/vitest.config.mts | 3 +- docker-compose.base.yml | 47 +- docker-compose.dev.yml | 98 +- docker-compose.preview.yml | 48 + docker-compose.test.yml | 96 +- docs/.gitignore | 43 + docs/README.md | 5 + docs/content/_meta.ts | 7 + .../content/development/db.mdx | 6 +- docs/content/development/getting-started.mdx | 171 + docs/content/development/kysely.mdx | 153 + .../content/development/serverActions.mdx | 8 +- docs/content/index.mdx | 9 + docs/content/infrastructure/ecs-cluster.mdx | 88 + .../environments/cloudflare.mdx | 24 + .../environments/global-aws.mdx | 21 + docs/content/infrastructure/index.mdx | 10 + .../infrastructure/modules/core-services.mdx | 108 + docs/content/infrastructure/nginx.mdx | 66 + docs/eslint.config.mjs | 20 + docs/mdx-components.tsx | 12 + docs/next.config.ts | 31 + docs/package.json | 37 + docs/postcss.config.mjs | 5 + docs/public/file.svg | 1 + docs/public/globe.svg | 1 + docs/public/logo.svg | 8 + docs/public/next.svg | 1 + docs/public/vercel.svg | 1 + docs/public/window.svg | 1 + docs/src/app/[[...mdxPath]]/page.tsx | 28 + docs/src/app/favicon.ico | Bin 0 -> 15406 bytes docs/src/app/globals.css | 18 + docs/src/app/layout.tsx | 53 + docs/src/app/logo.svg | 8 + docs/tsconfig.json | 27 + docs/utils/path.ts | 9 + .../global_aws/github_actions_iam.tf | 30 +- .../terraform/modules/core-services/main.tf | 8 + package.json | 158 +- packages/context-editor/package.json | 41 +- packages/context-editor/src/ContextEditor.tsx | 48 +- .../context-editor/src/commands/blocks.ts | 88 + packages/context-editor/src/commands/marks.ts | 32 + packages/context-editor/src/commands/math.ts | 82 + packages/context-editor/src/commands/types.ts | 48 + packages/context-editor/src/commands/util.ts | 27 + .../src/components/AttributePanel.tsx | 7 + .../context-editor/src/components/MenuBar.tsx | 232 + .../plugins/code/codeMirrorBlockNodeView.ts | 226 + .../src/plugins/code/defaults.ts | 82 + .../context-editor/src/plugins/code/index.ts | 114 + .../src/plugins/code/languageLoaders.ts | 24 + .../src/plugins/code/languages.ts | 21 + .../src/plugins/code/parsers.ts | 34 + .../context-editor/src/plugins/code/types.ts | 60 + .../context-editor/src/plugins/code/utils.ts | 156 + packages/context-editor/src/plugins/index.ts | 8 +- .../src/plugins/inputRules.test.ts | 87 + .../context-editor/src/plugins/inputRules.ts | 82 + packages/context-editor/src/plugins/lorem.ts | 34 - .../src/plugins/structureDecorations.ts | 7 +- .../context-editor/src/schemas/blockquote.ts | 32 + packages/context-editor/src/schemas/code.ts | 59 + packages/context-editor/src/schemas/index.ts | 11 +- packages/context-editor/src/schemas/math.ts | 52 + .../src/stories/EditorDash/PubsPanel.tsx | 35 +- .../src/stories/initialPubs.json | 18 +- packages/context-editor/src/style.css | 35 +- packages/context-editor/src/utils/nodes.ts | 19 + packages/context-editor/vitest.config.mts | 19 + packages/contracts/package.json | 1 + packages/contracts/src/resources/site.ts | 269 +- packages/contracts/src/resources/types.ts | 41 +- .../src/kanel/kanel-cleanup-enum-comments.cjs | 4 +- .../src/kanel/kanel-history-table-generic.cjs | 2 - .../kanel-kysely-zod-compatibility-hook.cjs | 7 +- .../db/src/public/ApiAccessPermissions.ts | 10 + packages/db/src/public/ApiAccessTokens.ts | 5 + packages/db/src/public/AuthTokens.ts | 5 + packages/db/src/public/FormElements.ts | 15 + packages/db/src/public/Forms.ts | 10 + packages/db/src/public/InputComponent.ts | 1 + packages/db/src/public/PubFieldToPubType.ts | 10 + packages/db/src/public/PubValues.ts | 5 + packages/db/src/public/PublicSchema.ts | 56 +- packages/db/src/public/Pubs.ts | 5 + packages/db/src/public/PubsInStages.ts | 10 + packages/db/src/public/Rules.ts | 10 + packages/db/src/table-names.ts | 136 + packages/schemas/package.json | 6 +- packages/schemas/src/formats.ts | 2 +- packages/schemas/src/index.ts | 2 +- packages/schemas/src/schemaComponents.ts | 30 +- packages/schemas/src/schemas.ts | 5 +- packages/ui/README.md | 48 +- packages/ui/components.json | 5 +- packages/ui/hooks/use-mobile.tsx | 19 + packages/ui/package.json | 14 + packages/ui/src/alert-dialog.tsx | 4 +- packages/ui/src/alert.tsx | 4 +- packages/ui/src/autocomplete.tsx | 3 +- packages/ui/src/avatar.tsx | 2 +- packages/ui/src/button.tsx | 14 +- packages/ui/src/command.tsx | 50 +- .../customRenderers/fileUpload/fileUpload.tsx | 76 +- packages/ui/src/dropdown-menu.tsx | 14 +- packages/ui/src/form.tsx | 6 +- packages/ui/src/hooks/useMobile.tsx | 19 + packages/ui/src/hover-card.tsx | 24 +- packages/ui/src/icon.tsx | 147 + packages/ui/src/input.tsx | 9 +- packages/ui/src/kbd.tsx | 35 + packages/ui/src/multiblock.tsx | 69 + packages/ui/src/popover.tsx | 2 +- packages/ui/src/pubTypes/PubTypesContext.tsx | 18 + packages/ui/src/pubTypes/index.tsx | 1 + packages/ui/src/separator.tsx | 2 +- packages/ui/src/sheet.tsx | 17 +- packages/ui/src/sidebar.tsx | 760 ++ packages/ui/src/skeleton.tsx | 7 +- packages/ui/src/table.tsx | 8 +- packages/ui/src/tooltip.tsx | 37 +- packages/ui/styles.css | 28 + packages/ui/tailwind.config.cjs | 31 +- packages/utils/package.json | 15 + packages/utils/src/uuid.ts | 5 + pnpm-lock.yaml | 7915 +++++++++++------ pnpm-workspace.yaml | 6 +- self-host/.gitignore | 2 + self-host/README.md | 300 + self-host/caddy/Caddyfile | 30 + self-host/caddy/Caddyfile.test | 37 + self-host/create-admin.sh | 21 + self-host/docker-compose.yml | 137 + self-host/minio/.gitkeep | 0 storybook/package.json | 2 +- 370 files changed, 23335 insertions(+), 6918 deletions(-) create mode 100644 .env.docker-compose.dev create mode 100644 .env.docker-compose.test create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-docs.yml create mode 100644 .github/workflows/pull-preview.yml rename core/actions/email/config/{recipient.field.tsx => recipientMember.field.tsx} (58%) rename core/actions/email/params/{recipient.field.tsx => recipientMember.field.tsx} (58%) create mode 100644 core/actions/googleDriveImport/discussionSchema.ts delete mode 100644 core/app/c/[communitySlug]/NavLink.module.css create mode 100644 core/app/c/[communitySlug]/NavLinkSubMenu.tsx delete mode 100644 core/app/c/[communitySlug]/pubs/[pubId]/components/PubChildrenTable.tsx delete mode 100644 core/app/c/[communitySlug]/pubs/[pubId]/components/PubChildrenTableWrapper.tsx delete mode 100644 core/app/c/[communitySlug]/pubs/[pubId]/components/getPubChildrenTableColumns.tsx delete mode 100644 core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts delete mode 100644 core/app/c/[communitySlug]/pubs/[pubId]/components/types.ts create mode 100644 core/app/components/FormBuilder/ElementPanel/ComponentConfig/RelationBlock.tsx create mode 100644 core/app/components/FormBuilder/FieldIcon.tsx create mode 100644 core/app/components/LastVisitedCommunity/SetLastVisited.tsx create mode 100644 core/app/components/LastVisitedCommunity/constants.ts create mode 100644 core/app/components/MemberSelect/MemberSelectClientFetch.tsx delete mode 100644 core/app/components/MemberSelect/MemberSelectServer.tsx create mode 100644 core/app/components/SearchDialog.tsx create mode 100644 core/app/components/SidePanel.tsx create mode 100644 core/app/components/forms/AddRelatedPubsPanel.tsx create mode 100644 core/app/components/forms/PubFieldFormElement.tsx create mode 100644 core/app/components/forms/elements/RelatedPubsElement.tsx create mode 100644 core/app/components/pubs/InitialCreatePubForm.tsx delete mode 100644 core/app/components/pubs/PubTypeFormClient.tsx delete mode 100644 core/kysely/README.md create mode 100644 core/kysely/database-init.ts create mode 100644 core/kysely/scripts/migrate-hierarchy.ts create mode 100644 core/kysely/updated-at-plugin.test.ts create mode 100644 core/lib/__tests__/fixtures/big-croc.jpg rename core/{ => lib/__tests__}/globalSetup.ts (83%) create mode 100644 core/lib/__tests__/matchers.ts create mode 100644 core/lib/server/assets.db.test.ts create mode 100644 core/lib/server/pub-op.db.test.ts create mode 100644 core/lib/server/pub-op.ts create mode 100644 core/lib/server/pub.fts.db.test.ts create mode 100644 core/lib/server/pub.sort.db.test.ts create mode 100644 core/lib/server/vitest.d.ts create mode 100644 core/lib/stages.test.ts create mode 100644 core/playwright/api/site.spec.ts create mode 100644 core/playwright/email.spec.ts create mode 100644 core/playwright/fileUpload.spec.ts create mode 100644 core/playwright/fixtures/api-token-page.ts create mode 100644 core/playwright/fixtures/test-assets/test-diagram.png create mode 100644 core/playwright/site-api.spec.ts create mode 100644 core/prisma/create-admin-user.cts delete mode 100644 core/prisma/db.ts create mode 100644 core/prisma/exampleCommunitySeeds/arcadiaJournal.ts create mode 100644 core/prisma/migrations/20250114195812_add_relation_block_input_component/migration.sql create mode 100644 core/prisma/migrations/20250130165541_add_ts_vector_to_pub_values/migration.sql create mode 100644 core/prisma/migrations/20250203230851_add_updated_at_to_everything/migration.sql create mode 100644 core/prisma/migrations/20250205172301_stage_updated_at_trigger/migration.sql create mode 100644 core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql create mode 100644 core/prisma/migrations/20250227001152_let_community_editors_invite_users/migration.sql create mode 100644 core/prisma/seed/createSeed.ts create mode 100644 docker-compose.preview.yml create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/content/_meta.ts rename packages/db/README.md => docs/content/development/db.mdx (93%) create mode 100644 docs/content/development/getting-started.mdx create mode 100644 docs/content/development/kysely.mdx rename core/lib/serverActions.md => docs/content/development/serverActions.mdx (94%) create mode 100644 docs/content/index.mdx create mode 100644 docs/content/infrastructure/ecs-cluster.mdx create mode 100644 docs/content/infrastructure/environments/cloudflare.mdx create mode 100644 docs/content/infrastructure/environments/global-aws.mdx create mode 100644 docs/content/infrastructure/index.mdx create mode 100644 docs/content/infrastructure/modules/core-services.mdx create mode 100644 docs/content/infrastructure/nginx.mdx create mode 100644 docs/eslint.config.mjs create mode 100644 docs/mdx-components.tsx create mode 100644 docs/next.config.ts create mode 100644 docs/package.json create mode 100644 docs/postcss.config.mjs create mode 100644 docs/public/file.svg create mode 100644 docs/public/globe.svg create mode 100644 docs/public/logo.svg create mode 100644 docs/public/next.svg create mode 100644 docs/public/vercel.svg create mode 100644 docs/public/window.svg create mode 100644 docs/src/app/[[...mdxPath]]/page.tsx create mode 100644 docs/src/app/favicon.ico create mode 100644 docs/src/app/globals.css create mode 100644 docs/src/app/layout.tsx create mode 100644 docs/src/app/logo.svg create mode 100644 docs/tsconfig.json create mode 100644 docs/utils/path.ts create mode 100644 packages/context-editor/src/commands/blocks.ts create mode 100644 packages/context-editor/src/commands/marks.ts create mode 100644 packages/context-editor/src/commands/math.ts create mode 100644 packages/context-editor/src/commands/types.ts create mode 100644 packages/context-editor/src/commands/util.ts create mode 100644 packages/context-editor/src/components/MenuBar.tsx create mode 100644 packages/context-editor/src/plugins/code/codeMirrorBlockNodeView.ts create mode 100644 packages/context-editor/src/plugins/code/defaults.ts create mode 100644 packages/context-editor/src/plugins/code/index.ts create mode 100644 packages/context-editor/src/plugins/code/languageLoaders.ts create mode 100644 packages/context-editor/src/plugins/code/languages.ts create mode 100644 packages/context-editor/src/plugins/code/parsers.ts create mode 100644 packages/context-editor/src/plugins/code/types.ts create mode 100644 packages/context-editor/src/plugins/code/utils.ts create mode 100644 packages/context-editor/src/plugins/inputRules.test.ts create mode 100644 packages/context-editor/src/plugins/inputRules.ts delete mode 100644 packages/context-editor/src/plugins/lorem.ts create mode 100644 packages/context-editor/src/schemas/blockquote.ts create mode 100644 packages/context-editor/src/schemas/code.ts create mode 100644 packages/context-editor/src/schemas/math.ts create mode 100644 packages/context-editor/src/utils/nodes.ts create mode 100644 packages/context-editor/vitest.config.mts create mode 100644 packages/ui/hooks/use-mobile.tsx create mode 100644 packages/ui/src/hooks/useMobile.tsx create mode 100644 packages/ui/src/kbd.tsx create mode 100644 packages/ui/src/multiblock.tsx create mode 100644 packages/ui/src/pubTypes/PubTypesContext.tsx create mode 100644 packages/ui/src/pubTypes/index.tsx create mode 100644 packages/ui/src/sidebar.tsx create mode 100644 packages/utils/src/uuid.ts create mode 100644 self-host/.gitignore create mode 100644 self-host/README.md create mode 100644 self-host/caddy/Caddyfile create mode 100644 self-host/caddy/Caddyfile.test create mode 100755 self-host/create-admin.sh create mode 100644 self-host/docker-compose.yml create mode 100644 self-host/minio/.gitkeep diff --git a/.env.docker-compose.dev b/.env.docker-compose.dev new file mode 100644 index 0000000000..2a3d834c4b --- /dev/null +++ b/.env.docker-compose.dev @@ -0,0 +1,13 @@ +MINIO_ROOT_USER=pubpub-minio-admin +MINIO_ROOT_PASSWORD=pubpub-minio-admin + +ASSETS_BUCKET_NAME=assets.v7.pubpub.org +ASSETS_UPLOAD_KEY=pubpubuser +ASSETS_UPLOAD_SECRET_KEY=pubpubpass +ASSETS_REGION=us-east-1 + +POSTGRES_PORT=54322 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres + diff --git a/.env.docker-compose.test b/.env.docker-compose.test new file mode 100644 index 0000000000..5fc11cef4e --- /dev/null +++ b/.env.docker-compose.test @@ -0,0 +1,40 @@ +MINIO_ROOT_USER=pubpub-minio-admin +MINIO_ROOT_PASSWORD=pubpub-minio-admin + +ASSETS_BUCKET_NAME=byron.v7.pubpub.org +ASSETS_UPLOAD_KEY=pubpubuserrr +ASSETS_UPLOAD_SECRET_KEY=pubpubpass +ASSETS_REGION=us-east-1 +ASSETS_STORAGE_ENDPOINT=http://localhost:9000 + +POSTGRES_PORT=54323 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres +POSTGRES_HOST=db + +# annoying duplication because jobs uses this version +PGHOST=db +PGPORT=5432 +PGUSER=postgres +PGPASSWORD=postgres +PGDATABASE=postgres + +# this needs to be db:5432 bc that's what it is in the app-network +# if you are running this from outside the docker network, you need to use +# @localhost:${POSTGRES_PORT} instead +DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres + + +JWT_SECRET=xxx +MAILGUN_SMTP_PASSWORD=xxx +GCLOUD_KEY_FILE=xxx + +MAILGUN_SMTP_HOST=inbucket +MAILGUN_SMTP_PORT=2500 +# this needs to be localhost:54324 instead of inbucket:9000 bc we are almost always running the integration tests from outside the docker network +INBUCKET_URL=http://localhost:54324 +MAILGUN_SMTP_USERNAME=omitted +OTEL_SERVICE_NAME=core.core +PUBPUB_URL=http://localhost:3000 +API_KEY=xxx diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..435c7f4893 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,55 @@ +# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json +# Dependabot configuration file +# See documentation: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # package.json + pnpm catalog updates + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "npm" + include: "scope" + # group all minor and patch updates together + groups: + minor-patch-dependencies: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # GitHub Actions updates + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "github-actions" + include: "scope" + + # docker updates + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "docker" + include: "scope" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000000..48bc99a699 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,80 @@ +name: Build Docs + +on: + workflow_call: + inputs: + preview: + type: boolean + required: true + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + with: + # necessary in order to show latest updates in docs + fetch-depth: 0 + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.13.1 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Get pnpm store directory + id: get-store-path + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.get-store-path.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + # - name: Cache turbo + # uses: actions/cache@v4 + # with: + # path: .turbo + # key: ${{ runner.os }}-turbo-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-turbo- + + - name: Install dependencies + run: pnpm install --frozen-lockfile --prefer-offline + + - name: set pr number if preview + id: set-pr-number + if: inputs.preview == true + run: | + echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + + - name: Build docs + env: + PR_NUMBER: ${{ steps.set-pr-number.outputs.PR_NUMBER }} + run: pnpm --filter docs build + + - name: Deploy docs main 🚀 + if: inputs.preview == false + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: docs/out + branch: gh-pages + clean-exclude: pr-preview + force: false + + - name: Deploy docs preview + if: inputs.preview == true + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: docs/out + action: deploy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64d487d05f..37565ca67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: runs-on: ubuntu-latest env: COMPOSE_FILE: docker-compose.test.yml + ENV_FILE: .env.docker-compose.test steps: - name: Checkout uses: actions/checkout@v4 @@ -26,7 +27,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22.13.1 - uses: pnpm/action-setup@v4 name: Install pnpm @@ -55,8 +56,8 @@ jobs: restore-keys: | ${{ runner.os }}-turbo- - - name: Start up DB - run: docker compose --profile test up -d + - name: Start test dependencies + run: pnpm test:setup - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline @@ -66,11 +67,6 @@ jobs: - name: Run migrations run: pnpm --filter core migrate-test - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5433/postgres - - - name: generate prisma - run: pnpm --filter core prisma generate - name: Run prettier run: pnpm format diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8b05f8fbde..4d2e9d5b2b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,6 +20,8 @@ jobs: integration-tests: name: Integration tests runs-on: ubuntu-latest + env: + ENV_FILE: .env.docker-compose.test steps: - name: Checkout uses: actions/checkout@v4 @@ -27,7 +29,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22.13.1 - uses: pnpm/action-setup@v4 name: Install pnpm @@ -48,31 +50,6 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline - - - name: Start up DB - run: docker compose -f docker-compose.test.yml --profile test up -d - - - name: p:build - run: pnpm p:build - - - name: Run migrations - run: pnpm --filter core prisma migrate deploy - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5433/postgres - - - name: generate prisma - run: pnpm --filter core prisma generate - - - name: seed db - run: pnpm --filter core prisma db seed - env: - # 20241126: this prevents the arcadia seed from running, which contains a ton of pubs which potentially might slow down the tests - MINIMAL_SEED: true - SKIP_VALIDATION: true - DATABASE_URL: postgresql://postgres:postgres@localhost:5433/postgres - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -100,10 +77,26 @@ jobs: echo "jobs_label=$ECR_REGISTRY/${ECR_REPOSITORY_NAME_OVERRIDE:-$ECR_REPOSITORY_PREFIX-jobs}:$IMAGE_TAG" >> $GITHUB_OUTPUT echo "base_label=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX:$IMAGE_TAG" >> $GITHUB_OUTPUT + - name: Install dependencies + run: pnpm install --frozen-lockfile --prefer-offline + + - name: Start up db images + run: pnpm test:setup + + - name: p:build + run: pnpm p:build + + - name: Run migrations and seed + run: pnpm --filter core db:test:reset + env: + # 20241126: this prevents the arcadia seed from running, which contains a ton of pubs which potentially might slow down the tests + MINIMAL_SEED: true + SKIP_VALIDATION: true + - run: pnpm --filter core exec playwright install chromium --with-deps - - name: Start up core - run: docker compose -f docker-compose.test.yml --profile integration up -d + - name: Start up core etc + run: pnpm integration:setup env: INTEGRATION_TESTS_IMAGE: ${{steps.label.outputs.core_label}} JOBS_IMAGE: ${{steps.label.outputs.jobs_label}} @@ -121,7 +114,7 @@ jobs: INTEGRATION_TEST_HOST: localhost - name: Print container logs - if: failure() + if: ${{failure() || cancelled()}} run: docker compose -f docker-compose.test.yml --profile integration logs - name: Upload playwright snapshots artifact diff --git a/.github/workflows/ecrbuild-all.yml b/.github/workflows/ecrbuild-all.yml index 154f87bca6..c7a76f039f 100644 --- a/.github/workflows/ecrbuild-all.yml +++ b/.github/workflows/ecrbuild-all.yml @@ -9,6 +9,20 @@ on: required: true AWS_SECRET_ACCESS_KEY: required: true + inputs: + publish_to_ghcr: + type: boolean + default: false + outputs: + core-image: + description: "Core image SHA" + value: ${{ jobs.build-core.outputs.image-sha }} + base-image: + description: "Base image SHA" + value: ${{ jobs.build-base.outputs.image-sha }} + jobs-image: + description: "Jobs image SHA" + value: ${{ jobs.build-jobs.outputs.image-sha }} jobs: emit-sha-tag: @@ -26,6 +40,9 @@ jobs: build-base: uses: ./.github/workflows/ecrbuild-template.yml + with: + publish_to_ghcr: ${{ inputs.publish_to_ghcr }} + ghcr_image_name: platform-migrations secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -36,6 +53,8 @@ jobs: # - build-base with: package: core + publish_to_ghcr: ${{ inputs.publish_to_ghcr }} + ghcr_image_name: platform # we require a bigger lad # We are now public, default public runner is big enough # runner: ubuntu-latest-m @@ -50,6 +69,8 @@ jobs: with: package: jobs target: jobs + publish_to_ghcr: ${{ inputs.publish_to_ghcr }} + ghcr_image_name: platform-jobs secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/ecrbuild-template.yml b/.github/workflows/ecrbuild-template.yml index 89b2c30b52..5aa8934d17 100644 --- a/.github/workflows/ecrbuild-template.yml +++ b/.github/workflows/ecrbuild-template.yml @@ -12,6 +12,16 @@ on: default: ubuntu-latest target: type: string + publish_to_ghcr: + type: boolean + default: false + ghcr_image_name: + type: string + required: false + outputs: + image-sha: + description: "Image SHA" + value: ${{ jobs.build.outputs.image-sha }} secrets: AWS_ACCESS_KEY_ID: required: true @@ -28,6 +38,8 @@ jobs: build: name: Build runs-on: ${{ inputs.runner }} + outputs: + image-sha: ${{ steps.label.outputs.label }} steps: - name: Checkout @@ -45,6 +57,13 @@ jobs: id: login-ecr uses: aws-actions/amazon-ecr-login@v2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # necessary in order to upload build source maps to sentry - name: Get sentry token id: sentry-token @@ -75,6 +94,16 @@ jobs: echo "target=${TARGET:-next-app-${PACKAGE}}" >> $GITHUB_OUTPUT fi echo "label=$ECR_REGISTRY/$ECR_REPOSITORY_PREFIX$package_suffix:$sha_short" >> $GITHUB_OUTPUT + if [[ ${{ inputs.publish_to_ghcr }} == "true" && -n ${{ inputs.ghcr_image_name }} ]] + then + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + + echo "ghcr_latest_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:latest" >> $GITHUB_OUTPUT + + echo "ghcr_sha_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$sha_short" >> $GITHUB_OUTPUT + + echo "ghcr_timestamp_label=ghcr.io/pubpub/${{ inputs.ghcr_image_name }}:$TIMESTAMP" >> $GITHUB_OUTPUT + fi - name: Check if SENTRY_AUTH_TOKEN is set run: | @@ -85,7 +114,7 @@ jobs: fi - name: Build, tag, and push image to Amazon ECR - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 id: build-image env: REGISTRY_REF: ${{steps.login-ecr.outputs.registry}}/${{env.ECR_REPOSITORY_PREFIX}}-${{env.PACKAGE}}:cache @@ -103,6 +132,10 @@ jobs: secrets: | SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} target: ${{ steps.label.outputs.target }} - tags: ${{ steps.label.outputs.label }} + tags: | + ${{ steps.label.outputs.label }} + ${{ steps.label.outputs.ghcr_latest_label }} + ${{ steps.label.outputs.ghcr_sha_label }} + ${{ steps.label.outputs.ghcr_timestamp_label }} platforms: linux/amd64 push: true diff --git a/.github/workflows/on_main.yml b/.github/workflows/on_main.yml index 99b0bcb9b1..976083570b 100644 --- a/.github/workflows/on_main.yml +++ b/.github/workflows/on_main.yml @@ -14,6 +14,8 @@ jobs: build-all: needs: ci uses: ./.github/workflows/ecrbuild-all.yml + with: + publish_to_ghcr: true secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -38,3 +40,12 @@ jobs: secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + + deploy-docs: + permissions: + contents: write + pages: write + pull-requests: write + uses: ./.github/workflows/build-docs.yml + with: + preview: false diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 0b4176a88d..edffc7fd1c 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -4,22 +4,43 @@ name: PR Updated triggers on: pull_request: - types: - - opened - - synchronize + types: [labeled, unlabeled, synchronize, closed, reopened, opened] env: AWS_REGION: us-east-1 +permissions: + id-token: write + contents: read + jobs: + path-filter: + runs-on: ubuntu-latest + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'closed' + outputs: + docs: ${{ steps.changes.outputs.docs }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + docs: + - 'docs/**' + ci: + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview') uses: ./.github/workflows/ci.yml + build-all: + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'preview') uses: ./.github/workflows/ecrbuild-all.yml secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + e2e: + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' needs: - build-all # could theoretically be skipped, but in practice is always faster @@ -29,3 +50,72 @@ jobs: secrets: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + + deploy-preview: + uses: ./.github/workflows/pull-preview.yml + needs: + - build-all + permissions: + contents: read + deployments: write + pull-requests: write + statuses: write + with: + PLATFORM_IMAGE: ${{ needs.build-all.outputs.core-image }} + JOBS_IMAGE: ${{ needs.build-all.outputs.jobs-image }} + MIGRATIONS_IMAGE: ${{ needs.build-all.outputs.base-image }} + PUBLIC_URL: ${{ github.event.repository.html_url }} + AWS_REGION: "us-east-1" + secrets: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + GH_PAT_PR_PREVIEW_CLEANUP: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} + + close-preview: + uses: ./.github/workflows/pull-preview.yml + if: github.event.action == 'closed' || (github.event.action == 'unlabeled' && github.event.label.name == 'preview') + permissions: + contents: read + deployments: write + pull-requests: write + statuses: write + with: + PLATFORM_IMAGE: "x" # not used + JOBS_IMAGE: "x" # not used + MIGRATIONS_IMAGE: "x" # not used + PUBLIC_URL: ${{ github.event.repository.html_url }} + AWS_REGION: "us-east-1" + secrets: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + GH_PAT_PR_PREVIEW_CLEANUP: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} + + deploy-docs-preview: + permissions: + contents: write + pages: write + pull-requests: write + needs: + - path-filter + if: (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') && needs.path-filter.outputs.docs == 'true' + uses: ./.github/workflows/build-docs.yml + with: + preview: true + + close-docs-preview: + needs: + - path-filter + permissions: + contents: write + pages: write + pull-requests: write + if: github.event.action == 'closed' && needs.path-filter.outputs.docs == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Close docs preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: docs/out + action: remove diff --git a/.github/workflows/pull-preview.yml b/.github/workflows/pull-preview.yml new file mode 100644 index 0000000000..65b11f4dba --- /dev/null +++ b/.github/workflows/pull-preview.yml @@ -0,0 +1,77 @@ +on: + workflow_call: + inputs: + PLATFORM_IMAGE: + required: true + type: string + JOBS_IMAGE: + required: true + type: string + MIGRATIONS_IMAGE: + required: true + type: string + PUBLIC_URL: + required: true + type: string + AWS_REGION: + required: true + type: string + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + GH_PAT_PR_PREVIEW_CLEANUP: + required: true + +permissions: + contents: read + deployments: write + pull-requests: write + statuses: write + +jobs: + preview: + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Copy .env file + run: cp ./self-host/.env.example ./self-host/.env + + - name: Configure pullpreview + env: + PLATFORM_IMAGE: ${{ inputs.PLATFORM_IMAGE }} + JOBS_IMAGE: ${{ inputs.JOBS_IMAGE }} + MIGRATIONS_IMAGE: ${{ inputs.MIGRATIONS_IMAGE }} + run: | + sed -i "s|image: PLATFORM_IMAGE|image: $PLATFORM_IMAGE|" docker-compose.preview.yml + sed -i "s|image: JOBS_IMAGE|image: $JOBS_IMAGE|" docker-compose.preview.yml + sed -i "s|image: MIGRATIONS_IMAGE|image: $MIGRATIONS_IMAGE|" docker-compose.preview.yml + sed -i "s|email someone@example.com|email dev@pubpub.org|" self-host/caddy/Caddyfile + sed -i "s|example.com|{\$PUBLIC_URL}|" self-host/caddy/Caddyfile + + - name: Get ECR token + id: ecrtoken + run: echo "value=$(aws ecr get-login-password --region us-east-1)" >> $GITHUB_OUTPUT + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: "us-east-1" + + - uses: pullpreview/action@v5 + with: + label: preview + admins: 3mcd + compose_files: ./self-host/docker-compose.yml,docker-compose.preview.yml + default_port: 443 + instance_type: small + ports: 80,443,9001 + registries: docker://AWS:${{steps.ecrtoken.outputs.value}}@246372085946.dkr.ecr.us-east-1.amazonaws.com + github_token: ${{ secrets.GH_PAT_PR_PREVIEW_CLEANUP }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ inputs.AWS_REGION }} + PULLPREVIEW_LOGGER_LEVEL: DEBUG diff --git a/.gitignore b/.gitignore index 288bdc3eab..ccc4f294ca 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,6 @@ core/supabase/.temp *storybook.log storybook-static -./playwright \ No newline at end of file +./playwright + +.local_data diff --git a/.nvmrc b/.nvmrc index 9aef5aab68..4377937757 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.17.0 \ No newline at end of file +v22.13.1 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7636918d84..e102df0a13 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,8 @@ "ms-playwright.playwright", "YoavBls.pretty-ts-errors", "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + // for yaml autocompletion using the # yaml-language-server: $schema=... directive + "redhat.vscode-yaml" ] } diff --git a/Dockerfile b/Dockerfile index ee68193b00..f23946bb53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ -ARG NODE_VERSION=20.17.0 +ARG NODE_VERSION=22.13.1 +ARG ALPINE_VERSION=3.20 ARG PACKAGE ARG PORT=3000 @@ -14,7 +15,7 @@ ARG PNPM_VERSION=9.10.0 ################################################################################ # Use node image for base image for all stages. -FROM node:${NODE_VERSION}-alpine as base +FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base # these are necessary to be able to use them inside of `base` ARG BASE_IMAGE diff --git a/README.md b/README.md index 4eb2c7ba2a..bf6bf52308 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ root - `jobs` holds the job queueing and scheduling service used by `core`. - `packages` holds libraries and npm packages that are shared by `core`, `jobs`, and `infrastructure`. -To avoid inconsistencies and difficult-to-track errors, we specify a particular version of node in `/.nvmrc` (currently `v20.17.0`). We recommend using [nvm](https://github.com/nvm-sh/nvm) to ensure you're using the same version. +To avoid inconsistencies and difficult-to-track errors, we specify a particular version of node in `/.nvmrc` (currently `v22.13.1`). We recommend using [nvm](https://github.com/nvm-sh/nvm) to ensure you're using the same version. ## Local Installation -This package runs the version of node specified in `.nvmrc` (currently `v20.17.0`) and uses pnpm for package management. All following commands are run from the root of this package. +This package runs the version of node specified in `.nvmrc` (currently `v22.13.1`) and uses pnpm for package management. All following commands are run from the root of this package. To get started, clone the repository and install the version of node specified in `.nvmrc` (we recommend using [nvm](https://github.com/nvm-sh/nvm). @@ -52,7 +52,7 @@ pnpm install pnpm build ``` -**Running build when getting started with this repo is important to make sure the any prebuild scripts run (e.g. Prisma generate).** +**Running build when getting started with this repo is important to make sure the any prebuild scripts run** Depending on which app or package you are doing work on, you may need to create a .env.local file. See each package's individual README.md file for further details. diff --git a/config/prettier/index.js b/config/prettier/index.js index dca76965a6..310c789aed 100644 --- a/config/prettier/index.js +++ b/config/prettier/index.js @@ -18,7 +18,7 @@ const config = { // "prettier-plugin-jsdoc", ], // tailwindConfig: fileURLToPath( - // new URL("../../tooling/tailwind/web.ts", import.meta.url), + // new URL("../../packages/ui/tailwind.config.cjs", import.meta.url) // ), tailwindFunctions: ["cn", "cva"], importOrder: [ diff --git a/core/.env.development b/core/.env.development index eee1816915..1ef1fadb7c 100644 --- a/core/.env.development +++ b/core/.env.development @@ -6,8 +6,11 @@ PUBPUB_URL="http://localhost:3000" ASSETS_BUCKET_NAME="assets.v7.pubpub.org" ASSETS_REGION="us-east-1" -ASSETS_UPLOAD_KEY="xxx" -ASSETS_UPLOAD_SECRET_KEY="xxx" +# mninio defaults +ASSETS_UPLOAD_KEY="pubpubuser" +ASSETS_UPLOAD_SECRET_KEY="pubpubpass" +ASSETS_STORAGE_ENDPOINT="http://localhost:9000" + MAILGUN_SMTP_PASSWORD="xxx" MAILGUN_SMTP_USERNAME="xxx" diff --git a/core/.env.test b/core/.env.test index 9f486764fa..a030cfd46b 100644 --- a/core/.env.test +++ b/core/.env.test @@ -1,13 +1,15 @@ -DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres +DATABASE_URL=postgresql://postgres:postgres@localhost:54323/postgres PUBPUB_URL=http://localhost:3000 MAILGUN_SMTP_HOST=localhost MAILGUN_SMTP_PORT=54325 API_KEY="super_secret_key" -ASSETS_BUCKET_NAME="assets.v7.pubpub.org" -ASSETS_REGION="us-east-1" -ASSETS_UPLOAD_KEY="xxx" -ASSETS_UPLOAD_SECRET_KEY="xxx" +ASSETS_BUCKET_NAME=byron.v7.pubpub.org +ASSETS_UPLOAD_KEY=pubpubuserrr +ASSETS_UPLOAD_SECRET_KEY=pubpubpass +ASSETS_REGION=us-east-1 +ASSETS_STORAGE_ENDPOINT="http://localhost:9000" + MAILGUN_SMTP_PASSWORD="xxx" MAILGUN_SMTP_USERNAME="xxx" @@ -17,3 +19,4 @@ HONEYCOMB_API_KEY="xxx" KYSELY_DEBUG="true" GCLOUD_KEY_FILE='xxx' + diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml index f314305dcd..b050f147ee 100644 --- a/core/.github/workflows/playwright.yml +++ b/core/.github/workflows/playwright.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: lts/* + node-version: 22.13.1 - name: Install dependencies run: npm install -g pnpm && pnpm install - name: Install Playwright Browsers diff --git a/core/README.md b/core/README.md index 1b62053e5e..58a5cea34b 100644 --- a/core/README.md +++ b/core/README.md @@ -38,6 +38,8 @@ pnpm run dev ## Prisma +We currently only use Prisma for managing migrations. + The Prisma [Quickstart guide](https://www.prisma.io/docs/getting-started/quickstart) is how our prisma folder was initially created. That set of instructions has useful pointers for doing things like db migrations. The `~/prisma/seed.ts` file will initiate the database with a set of data. This seed is run using `pnpm reset`. You will have to run this each time you stop and start supabase since doing so clears the database. @@ -46,13 +48,13 @@ Explore with `pnpm prisma-studio`. ## Folder structure -- `/actions` Configuration/lib for the action framework. -- `/app` The Next.JS [app directory](https://nextjs.org/docs/app/building-your-application/routing). -- `/lib` Functions that are re-used in multiple locations throughout the codebase. Akin to a `/utils` folder. -- `/prisma` Config and functions for using Prisma -- `/kysely` Config and functions for using Kysely -- `/public` Static files that will be publicly available at `https://[URL]/`. -- `/playwright` End-to-end tests +- `/actions` Configuration/lib for the action framework. +- `/app` The Next.JS [app directory](https://nextjs.org/docs/app/building-your-application/routing). +- `/lib` Functions that are re-used in multiple locations throughout the codebase. Akin to a `/utils` folder. +- `/prisma` Config and functions for using Prisma +- `/kysely` Config and functions for using Kysely +- `/public` Static files that will be publicly available at `https://[URL]/`. +- `/playwright` End-to-end tests ## Authentication diff --git a/core/actions/_lib/runActionInstance.db.test.ts b/core/actions/_lib/runActionInstance.db.test.ts index 7e09147166..3d00083ea7 100644 --- a/core/actions/_lib/runActionInstance.db.test.ts +++ b/core/actions/_lib/runActionInstance.db.test.ts @@ -10,7 +10,7 @@ const { getTrx, rollback, commit } = createForEachMockedTransaction(); const pubTriggerTestSeed = async () => { const slugName = `test-server-pub-${new Date().toISOString()}`; - const { createSeed } = await import("~/prisma/seed/seedCommunity"); + const { createSeed } = await import("~/prisma/seed/createSeed"); return createSeed({ community: { diff --git a/core/actions/_lib/runActionInstance.ts b/core/actions/_lib/runActionInstance.ts index b4a6ea0b94..f0906716fb 100644 --- a/core/actions/_lib/runActionInstance.ts +++ b/core/actions/_lib/runActionInstance.ts @@ -18,7 +18,7 @@ import type { ClientException, ClientExceptionOptions } from "~/lib/serverAction import { db } from "~/kysely/database"; import { hydratePubValues } from "~/lib/fields/utils"; import { createLastModifiedBy } from "~/lib/lastModifiedBy"; -import { getPubsWithRelatedValuesAndChildren } from "~/lib/server"; +import { getPubsWithRelatedValues } from "~/lib/server"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { isClientException } from "~/lib/serverActions"; import { getActionByName } from "../api"; @@ -40,7 +40,7 @@ const _runActionInstance = async ( ): Promise => { const isActionUserInitiated = "userId" in args; - const pubPromise = getPubsWithRelatedValuesAndChildren( + const pubPromise = getPubsWithRelatedValues( { pubId: args.pubId, communityId: args.communityId, diff --git a/core/actions/datacite/action.ts b/core/actions/datacite/action.ts index 534a071315..65d84821c2 100644 --- a/core/actions/datacite/action.ts +++ b/core/actions/datacite/action.ts @@ -9,13 +9,13 @@ export const action = defineAction({ name: Action.datacite, config: { schema: z.object({ - doi: z.string(), + doi: z.string().optional(), doiPrefix: z.string().optional(), - doiSuffix: z.string(), + doiSuffix: z.string().optional(), title: z.string(), url: z.string(), publisher: z.string(), - publicationDate: z.date(), + publicationDate: z.coerce.date(), creator: z.string(), creatorName: z.string(), }), @@ -48,13 +48,13 @@ export const action = defineAction({ }, params: { schema: z.object({ - doi: z.string(), + doi: z.string().optional(), doiPrefix: z.string().optional(), - doiSuffix: z.string(), + doiSuffix: z.string().optional(), title: z.string(), url: z.string(), publisher: z.string(), - publicationDate: z.date(), + publicationDate: z.coerce.date(), creator: z.string(), creatorName: z.string(), }), diff --git a/core/actions/datacite/run.test.ts b/core/actions/datacite/run.test.ts index 2870ae09f7..1b3e44951c 100644 --- a/core/actions/datacite/run.test.ts +++ b/core/actions/datacite/run.test.ts @@ -32,7 +32,7 @@ vitest.mock("~/lib/env/env.mjs", () => { vitest.mock("~/lib/server", () => { return { - getPubsWithRelatedValuesAndChildren: () => { + getPubsWithRelatedValues: () => { return { ...pub, values: [] }; }, updatePub: vitest.fn(() => { @@ -110,7 +110,6 @@ const pub = { relatedPubId: null, }, ], - children: [], communityId: "" as CommunitiesId, createdAt: new Date(), updatedAt: new Date(), diff --git a/core/actions/datacite/run.ts b/core/actions/datacite/run.ts index a098a27b87..da5770bc62 100644 --- a/core/actions/datacite/run.ts +++ b/core/actions/datacite/run.ts @@ -6,11 +6,11 @@ import type { ProcessedPub } from "contracts"; import type { PubsId } from "db/public"; import { assert, AssertionError, expect } from "utils"; -import type { ActionPub, ActionPubType } from "../types"; +import type { ActionPub } from "../types"; import type { action } from "./action"; import type { components } from "./types"; import { env } from "~/lib/env/env.mjs"; -import { getPubsWithRelatedValuesAndChildren, updatePub } from "~/lib/server"; +import { getPubsWithRelatedValues, updatePub } from "~/lib/server"; import { isClientExceptionOptions } from "~/lib/serverActions"; import { defineRun } from "../types"; @@ -18,9 +18,7 @@ type ConfigSchema = z.infer<(typeof action)["config"]["schema"]>; type Config = ConfigSchema & { pubFields: { [K in keyof ConfigSchema]?: string[] } }; type Payload = components["schemas"]["Doi"]; -type RelatedPubs = Awaited< - ReturnType> ->[number]["values"]; +type RelatedPubs = Awaited>>[number]["values"]; const encodeDataciteCredentials = (username: string, password: string) => Buffer.from(`${username}:${password}`).toString("base64"); @@ -65,7 +63,7 @@ const makeDatacitePayload = async (pub: ActionPub, config: Config): Promise Boolean(recipientMember), + type: DependencyType.DISABLES, + }, + ], }, description: "Send an email to one or more users", params: { schema: z .object({ - recipient: z + recipientEmail: z.string().email().describe("Recipient email address").optional(), + recipientMember: z .string() .uuid() .describe( - "Recipient|Overrides the recipient user specified in the action config." + "Recipient Member|Overrides the recipient community member specified in the action config." ) .optional(), subject: stringWithTokens() @@ -46,10 +60,21 @@ export const action = defineAction({ }) .optional(), fieldConfig: { - recipient: { + recipientEmail: { + allowedSchemas: true, + }, + recipientMember: { fieldType: "custom", }, }, + dependencies: [ + { + sourceField: "recipientMember", + targetField: "recipientEmail", + when: (recipientMember) => Boolean(recipientMember), + type: DependencyType.DISABLES, + }, + ], }, icon: Mail, tokens: { diff --git a/core/actions/email/config/recipient.field.tsx b/core/actions/email/config/recipientMember.field.tsx similarity index 58% rename from core/actions/email/config/recipient.field.tsx rename to core/actions/email/config/recipientMember.field.tsx index 7b7f33d26b..4b1187d3ab 100644 --- a/core/actions/email/config/recipient.field.tsx +++ b/core/actions/email/config/recipientMember.field.tsx @@ -1,7 +1,7 @@ import type { CommunityMembershipsId } from "db/public"; import { defineActionFormFieldServerComponent } from "~/actions/_lib/custom-form-field/defineConfigServerComponent"; -import { MemberSelectServer } from "~/app/components/MemberSelect/MemberSelectServer"; +import { MemberSelectClientFetch } from "~/app/components/MemberSelect/MemberSelectClientFetch"; import { db } from "~/kysely/database"; import { autoCache } from "~/lib/server/cache/autoCache"; import { action } from "../action"; @@ -14,17 +14,12 @@ const component = defineActionFormFieldServerComponent( db.selectFrom("communities").selectAll().where("id", "=", communityId) ).executeTakeFirstOrThrow(); - const queryParamName = `recipient-${actionInstance.id?.split("-").pop()}`; - const query = pageContext.searchParams?.[queryParamName] as string | undefined; - return ( - ); } diff --git a/core/actions/email/params/recipient.field.tsx b/core/actions/email/params/recipientMember.field.tsx similarity index 58% rename from core/actions/email/params/recipient.field.tsx rename to core/actions/email/params/recipientMember.field.tsx index 945199dcc3..6473817e93 100644 --- a/core/actions/email/params/recipient.field.tsx +++ b/core/actions/email/params/recipientMember.field.tsx @@ -1,7 +1,7 @@ import type { CommunityMembershipsId } from "db/public"; import { defineActionFormFieldServerComponent } from "~/actions/_lib/custom-form-field/defineConfigServerComponent"; -import { MemberSelectServer } from "~/app/components/MemberSelect/MemberSelectServer"; +import { MemberSelectClientFetch } from "~/app/components/MemberSelect/MemberSelectClientFetch"; import { db } from "~/kysely/database"; import { autoCache } from "~/lib/server/cache/autoCache"; import { action } from "../action"; @@ -14,17 +14,12 @@ const component = defineActionFormFieldServerComponent( db.selectFrom("communities").selectAll().where("id", "=", communityId) ).executeTakeFirstOrThrow(); - const queryParamName = `recipient-${actionInstance.id?.split("-").pop()}`; - const query = pageContext.searchParams?.[queryParamName] as string | undefined; - return ( - ); } diff --git a/core/actions/email/run.ts b/core/actions/email/run.ts index f9ffd9a1f3..7818ee85fa 100644 --- a/core/actions/email/run.ts +++ b/core/actions/email/run.ts @@ -4,7 +4,7 @@ import { jsonObjectFrom } from "kysely/helpers/postgres"; import type { CommunityMembershipsId } from "db/public"; import { logger } from "logger"; -import { expect } from "utils"; +import { assert, expect } from "utils"; import type { action } from "./action"; import type { @@ -12,10 +12,11 @@ import type { RenderWithPubPub, } from "~/lib/server/render/pub/renderWithPubUtils"; import { db } from "~/kysely/database"; -import { getPubsWithRelatedValuesAndChildren } from "~/lib/server"; +import { getPubsWithRelatedValues } from "~/lib/server"; import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug"; import * as Email from "~/lib/server/email"; import { renderMarkdownWithPub } from "~/lib/server/render/pub/renderMarkdownWithPub"; +import { isClientException } from "~/lib/serverActions"; import { defineRun } from "../types"; export const run = defineRun(async ({ pub, config, args, communityId }) => { @@ -29,7 +30,7 @@ export const run = defineRun(async ({ pub, config, args, communit // will redundantly load the child pub. Ideally we would lazily fetch and // cache the parent pub while processing the email template. if (parentId) { - parentPub = await getPubsWithRelatedValuesAndChildren( + parentPub = await getPubsWithRelatedValues( { pubId: parentId, communityId }, { withPubType: true, @@ -38,28 +39,37 @@ export const run = defineRun(async ({ pub, config, args, communit ); } - const recipientId = expect(args?.recipient ?? config.recipient) as CommunityMembershipsId; - - // TODO: similar to the assignee, the recipient args/config should accept - // the pub assignee, a pub field, a static email address, a member, or a - // member group. - const recipient = await db - .selectFrom("community_memberships") - .select((eb) => [ - "community_memberships.id", - jsonObjectFrom( - eb - .selectFrom("users") - .whereRef("users.id", "=", "community_memberships.userId") - .selectAll("users") - ) - .$notNull() - .as("user"), - ]) - .where("id", "=", recipientId) - .executeTakeFirstOrThrow( - () => new Error(`Could not find member with ID ${recipientId}`) - ); + const recipientEmail = args?.recipientEmail ?? config.recipientEmail; + const recipientMemberId = (args?.recipientMember ?? config.recipientMember) as + | CommunityMembershipsId + | undefined; + + assert( + recipientEmail !== undefined || recipientMemberId !== undefined, + "No email recipient was specified" + ); + + let recipient: RenderWithPubContext["recipient"] | undefined; + + if (recipientMemberId !== undefined) { + recipient = await db + .selectFrom("community_memberships") + .select((eb) => [ + "community_memberships.id", + jsonObjectFrom( + eb + .selectFrom("users") + .whereRef("users.id", "=", "community_memberships.userId") + .selectAll("users") + ) + .$notNull() + .as("user"), + ]) + .where("id", "=", recipientMemberId) + .executeTakeFirstOrThrow( + () => new Error(`Could not find member with ID ${recipientMemberId}`) + ); + } const renderMarkdownWithPubContext = { communityId, @@ -79,13 +89,34 @@ export const run = defineRun(async ({ pub, config, args, communit true ); - await Email.generic({ - to: recipient.user.email, + const result = await Email.generic({ + to: expect(recipient?.user.email ?? recipientEmail), subject, html, }).send(); + + if (isClientException(result)) { + logger.error({ + msg: "An error occurred while sending an email", + error: result.error, + pub, + config, + args, + renderMarkdownWithPubContext, + }); + } else { + logger.info({ + msg: "Successfully sent email", + pub, + config, + args, + renderMarkdownWithPubContext, + }); + } + + return result; } catch (error) { - logger.error({ msg: "email", error }); + logger.error({ msg: "Failed to send email", error }); return { title: "Failed to Send Email", @@ -93,12 +124,4 @@ export const run = defineRun(async ({ pub, config, args, communit cause: error, }; } - - logger.info({ msg: "email", pub, config, args }); - - return { - success: true, - report: "Email sent", - data: {}, - }; }); diff --git a/core/actions/googleDriveImport/OutputField.tsx b/core/actions/googleDriveImport/OutputField.tsx index 7e03a5118e..8a24e4d1b4 100644 --- a/core/actions/googleDriveImport/OutputField.tsx +++ b/core/actions/googleDriveImport/OutputField.tsx @@ -1,8 +1,7 @@ "use client"; -import type { CoreSchemaType } from "@prisma/client"; - import type { PubFieldSchemaId, PubFieldsId } from "db/public"; +import { CoreSchemaType } from "db/public"; import { FormControl, FormField, FormItem, FormLabel } from "ui/form"; import { Info } from "ui/icon"; import { usePubFieldContext } from "ui/pubFields"; diff --git a/core/actions/googleDriveImport/config/outputField.field.tsx b/core/actions/googleDriveImport/config/outputField.field.tsx index b22f4ec946..c49a030c33 100644 --- a/core/actions/googleDriveImport/config/outputField.field.tsx +++ b/core/actions/googleDriveImport/config/outputField.field.tsx @@ -1,4 +1,4 @@ -import { CoreSchemaType } from "@prisma/client"; +import { CoreSchemaType } from "db/public"; import { defineActionFormFieldServerComponent } from "../../_lib/custom-form-field/defineConfigServerComponent"; import { action } from "../action"; diff --git a/core/actions/googleDriveImport/discussionSchema.ts b/core/actions/googleDriveImport/discussionSchema.ts new file mode 100644 index 0000000000..8683778110 --- /dev/null +++ b/core/actions/googleDriveImport/discussionSchema.ts @@ -0,0 +1,446 @@ +import type { DOMOutputSpec, Mark, Node, NodeSpec } from "prosemirror-model"; + +import { Schema } from "prosemirror-model"; + +export const baseNodes: { [key: string]: NodeSpec } = { + doc: { + content: "block+", + attrs: { + meta: { default: {} }, + }, + }, + paragraph: { + selectable: false, + // reactive: true, + content: "inline*", + group: "block", + attrs: { + id: { default: null }, + class: { default: null }, + textAlign: { default: null }, + rtl: { default: null }, + }, + parseDOM: [ + { + tag: "p", + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + class: (node as Element).getAttribute("class"), + textAlign: (node as Element).getAttribute("data-text-align"), + rtl: (node as Element).getAttribute("data-rtl"), + }; + }, + }, + ], + toDOM: (node) => { + const isEmpty = !node.content || (Array.isArray(node.content) && !node.content.length); + const children = isEmpty ? ["br"] : 0; + return [ + "p", + { + class: node.attrs.class, + ...(node.attrs.id && { id: node.attrs.id }), + ...(node.attrs.textAlign && { "data-text-align": node.attrs.textAlign }), + ...(node.attrs.rtl && { "data-rtl": node.attrs.rtl.toString() }), + }, + children, + ] as DOMOutputSpec; + }, + }, + blockquote: { + content: "block+", + group: "block", + attrs: { + id: { default: null }, + }, + selectable: false, + parseDOM: [ + { + tag: "blockquote", + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + }; + }, + }, + ], + toDOM: (node) => { + return [ + "blockquote", + { ...(node.attrs.id && { id: node.attrs.id }) }, + 0, + ] as DOMOutputSpec; + }, + }, + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + selectable: true, + toDOM: () => { + return ["div", ["hr"]] as DOMOutputSpec; + }, + }, + heading: { + attrs: { + level: { default: 1 }, + fixedId: { default: "" }, + id: { default: "" }, + textAlign: { default: null }, + rtl: { default: null }, + }, + content: "inline*", + group: "block", + defining: true, + selectable: false, + parseDOM: [1, 2, 3, 4, 5, 6].map((level) => { + return { + tag: `h${level}`, + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + textAlign: (node as Element).getAttribute("data-text-align"), + rtl: (node as Element).getAttribute("data-rtl"), + level, + }; + }, + }; + }), + toDOM: (node) => { + return [ + `h${node.attrs.level}`, + { + id: node.attrs.fixedId || node.attrs.id, + ...(node.attrs.textAlign && { "data-text-align": node.attrs.textAlign }), + ...(node.attrs.rtl && { "data-rtl": node.attrs.rtl.toString() }), + }, + 0, + ] as DOMOutputSpec; + }, + }, + ordered_list: { + content: "list_item+", + group: "block", + attrs: { + id: { default: null }, + order: { default: 1 }, + rtl: { default: null }, + }, + selectable: false, + parseDOM: [ + { + tag: "ol", + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + order: (node as Element).hasAttribute("start") + ? +(node as Element).getAttribute("start")! + : 1, + rtl: (node as Element).getAttribute("data-rtl"), + }; + }, + }, + ], + toDOM: (node) => { + return [ + "ol", + { + ...(node.attrs.id && { id: node.attrs.id }), + ...(node.attrs.textAlign && { "data-text-align": node.attrs.textAlign }), + ...(node.attrs.rtl && { "data-rtl": node.attrs.rtl.toString() }), + start: node.attrs.order === 1 ? null : node.attrs.order, + }, + 0, + ] as DOMOutputSpec; + }, + }, + bullet_list: { + content: "list_item+", + group: "block", + attrs: { + id: { default: null }, + rtl: { default: null }, + }, + selectable: false, + parseDOM: [ + { + tag: "ul", + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + rtl: (node as Element).getAttribute("data-rtl"), + }; + }, + }, + ], + toDOM: (node) => { + return [ + "ul", + { + ...(node.attrs.id && { id: node.attrs.id }), + ...(node.attrs.textAlign && { "data-text-align": node.attrs.textAlign }), + ...(node.attrs.rtl && { "data-rtl": node.attrs.rtl.toString() }), + }, + 0, + ] as DOMOutputSpec; + }, + }, + list_item: { + content: "paragraph block*", + defining: true, + selectable: false, + parseDOM: [{ tag: "li" }], + toDOM: () => { + return ["li", 0] as DOMOutputSpec; + }, + }, + text: { + inline: true, + group: "inline", + toDOM: (node) => { + return node.text!; + }, + }, + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM: () => { + return ["br"] as DOMOutputSpec; + }, + }, + image: { + atom: true, + reactive: true, + attrs: { + id: { default: null }, + url: { default: null }, + src: { default: null }, + size: { default: 50 }, // number as percentage + align: { default: "center" }, + caption: { default: "" }, + altText: { default: "" }, + hideLabel: { default: false }, + fullResolution: { default: false }, + href: { default: null }, + }, + parseDOM: [ + { + tag: "figure", + getAttrs: (node) => { + if (node.getAttribute("data-node-type") !== "image") { + return false; + } + return { + id: node.getAttribute("id") || null, + url: node.getAttribute("data-url") || null, + caption: node.getAttribute("data-caption") || "", + size: Number(node.getAttribute("data-size")) || 50, + align: node.getAttribute("data-align") || "center", + altText: node.getAttribute("data-alt-text") || "", + hideLabel: node.getAttribute("data-hide-label") || "", + href: node.getAttribute("data-href") || null, + }; + }, + }, + ], + toDOM: (node) => { + const { url, align, id, altText, caption, size, hideLabel, href } = node.attrs; + return [ + "figure", + { + ...(id && { id }), + "data-node-type": "image", + "data-size": size, + "data-align": align, + "data-url": url, + "data-caption": caption, + "data-href": href, + "data-alt-text": altText, + "data-hide-label": hideLabel, + }, + [ + "img", + { + src: url, + alt: altText || "", + }, + ], + ] as unknown as DOMOutputSpec; + }, + inline: false, + group: "block", + }, + file: { + atom: true, + attrs: { + id: { default: null }, + url: { default: null }, + fileName: { default: null }, + fileSize: { default: null }, + caption: { default: "" }, + }, + parseDOM: [ + { + tag: "figure", + getAttrs: (node) => { + if (node.getAttribute("data-node-type") !== "file") { + return false; + } + return { + id: node.getAttribute("id") || null, + url: node.getAttribute("data-url") || null, + fileName: node.getAttribute("data-file-name") || null, + fileSize: node.getAttribute("data-file-size") || null, + caption: node.getAttribute("data-caption") || "", + }; + }, + }, + ], + toDOM: (node: Node) => { + const attrs = node.attrs; + return [ + "p", + [ + "a", + { + href: attrs.url, + target: "_blank", + rel: "noopener noreferrer", + download: attrs.fileName, + class: `download`, + }, + attrs.fileName, + ], + ]; + }, + inline: false, + group: "block", + }, + code_block: { + content: "text*", + group: "block", + attrs: { + lang: { default: null }, + id: { default: null }, + }, + code: true, + selectable: false, + parseDOM: [ + { + tag: "pre", + getAttrs: (node) => { + return { + id: (node as Element).getAttribute("id"), + }; + }, + preserveWhitespace: "full" as const, + }, + ], + toDOM: (node: Node) => + ["pre", { ...(node.attrs.id && { id: node.attrs.id }) }, ["code", 0]] as DOMOutputSpec, + }, +}; + +export const baseMarks = { + em: { + parseDOM: [ + { tag: "i" }, + { tag: "em" }, + { + style: "font-style", + getAttrs: (value: string) => value === "italic" && null, + }, + ], + toDOM: () => { + return ["em"] as DOMOutputSpec; + }, + }, + + strong: { + parseDOM: [ + { tag: "strong" }, + /* + This works around a Google Docs misbehavior where + pasted content will be inexplicably wrapped in `` + tags with a font-weight normal. + */ + { + tag: "b", + getAttrs: (node: HTMLElement) => node.style.fontWeight !== "normal" && null, + }, + { + style: "font-weight", + getAttrs: (value: string) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null, + }, + ], + toDOM: () => { + return ["strong"] as DOMOutputSpec; + }, + }, + link: { + inclusive: false, + attrs: { + href: { default: "" }, + title: { default: null }, + target: { default: null }, + pubEdgeId: { default: null }, + }, + parseDOM: [ + { + tag: "a[href]", + getAttrs: (dom: HTMLElement) => { + if (dom.getAttribute("data-node-type") === "reference") { + return false; + } + return { + href: dom.getAttribute("href"), + title: dom.getAttribute("title"), + target: dom.getAttribute("target"), + pubEdgeId: dom.getAttribute("data-pub-edge-id"), + }; + }, + }, + ], + toDOM: (mark: Mark, inline: boolean) => { + let attrs = mark.attrs; + if (attrs.target && typeof attrs.target !== "string") { + attrs = { ...attrs, target: null }; + } + const { pubEdgeId, ...restAttrs } = attrs; + return ["a", { "data-pub-edge-id": pubEdgeId, ...restAttrs }] as DOMOutputSpec; + }, + }, + sub: { + parseDOM: [{ tag: "sub" }], + toDOM: () => { + return ["sub"] as DOMOutputSpec; + }, + }, + sup: { + parseDOM: [{ tag: "sup" }], + toDOM: () => { + return ["sup"] as DOMOutputSpec; + }, + }, + strike: { + parseDOM: [{ tag: "s" }, { tag: "strike" }, { tag: "del" }], + toDOM: () => { + return ["s"] as DOMOutputSpec; + }, + }, + code: { + parseDOM: [{ tag: "code" }], + toDOM: () => { + return ["code"] as DOMOutputSpec; + }, + }, +}; + +const mySchema = new Schema({ + nodes: baseNodes, + marks: baseMarks, +}); + +export default mySchema; diff --git a/core/actions/googleDriveImport/formatDriveData.ts b/core/actions/googleDriveImport/formatDriveData.ts index 5c56e846f2..af06913e4e 100644 --- a/core/actions/googleDriveImport/formatDriveData.ts +++ b/core/actions/googleDriveImport/formatDriveData.ts @@ -1,13 +1,26 @@ -import { writeFile } from "fs/promises"; +// import { writeFile } from "fs/promises"; +import type { Root } from "hast"; +import { defaultMarkdownSerializer } from "prosemirror-markdown"; +import { Node } from "prosemirror-model"; import { rehype } from "rehype"; import rehypeFormat from "rehype-format"; +import { visit } from "unist-util-visit"; import type { PubsId } from "db/public"; import type { DriveData } from "./getGDriveFiles"; +import { uploadFileToS3 } from "~/lib/server"; +import schema from "./discussionSchema"; import { + appendFigureAttributes, + cleanUnusedSpans, + formatFigureReferences, + formatLists, + getDescription, processLocalLinks, + removeDescription, + removeEmptyFigCaption, removeGoogleLinkForwards, removeVerboseFormatting, structureAnchors, @@ -23,10 +36,13 @@ import { structureInlineCode, structureInlineMath, structureReferences, + structureTables, structureVideos, } from "./gdocPlugins"; +import { getAssetFile } from "./getGDriveFiles"; export type FormattedDriveData = { + pubDescription: string; pubHtml: string; versions: { [description: `${string}:description`]: string; @@ -35,13 +51,74 @@ export type FormattedDriveData = { }[]; discussions: { id: PubsId; values: {} }[]; }; +const processAssets = async (html: string, pubId: string): Promise => { + const result = await rehype() + .use(() => async (tree: Root) => { + const assetUrls: { [key: string]: string } = {}; + visit(tree, "element", (node: any) => { + const hasSrc = ["img", "video", "audio", "source"].includes(node.tagName); + const isDownload = + node.tagName === "a" && node.properties.className === "file-button"; + if (hasSrc || isDownload) { + const propertyKey = hasSrc ? "src" : "href"; + const originalAssetUrl = node.properties[propertyKey]; + if (originalAssetUrl) { + const urlObject = new URL(originalAssetUrl); + if (urlObject.hostname !== "pubpub.org") { + assetUrls[originalAssetUrl] = ""; + } + } + } + }); + await Promise.all( + Object.keys(assetUrls).map(async (originalAssetUrl) => { + try { + const assetData = await getAssetFile(originalAssetUrl); + if (assetData) { + const uploadedUrl = await uploadFileToS3( + pubId, + assetData.filename, + assetData.buffer, + { contentType: assetData.mimetype } + ); + assetUrls[originalAssetUrl] = uploadedUrl.replace( + "assets.app.pubpub.org.s3.us-east-1.amazonaws.com", + "assets.app.pubpub.org" + ); + } else { + assetUrls[originalAssetUrl] = originalAssetUrl; + } + } catch (err) { + assetUrls[originalAssetUrl] = originalAssetUrl; + } + }) + ); + + visit(tree, "element", (node: any) => { + const hasSrc = ["img", "video", "audio"].includes(node.tagName); + const isDownload = + node.tagName === "a" && node.properties.className === "file-button"; + if (hasSrc || isDownload) { + const propertyKey = hasSrc ? "src" : "href"; + const originalAssetUrl = node.properties[propertyKey]; + if (assetUrls[originalAssetUrl]) { + node.properties[propertyKey] = assetUrls[originalAssetUrl]; + } + } + }); + }) + .process(html); + return String(result); +}; const processHtml = async (html: string): Promise => { const result = await rehype() .use(structureFormatting) + .use(formatLists) .use(removeVerboseFormatting) .use(removeGoogleLinkForwards) .use(processLocalLinks) + .use(formatFigureReferences) /* Assumes figures are still tables */ .use(structureImages) .use(structureVideos) .use(structureAudio) @@ -53,8 +130,13 @@ const processHtml = async (html: string): Promise => { .use(structureCodeBlock) .use(structureInlineCode) .use(structureAnchors) + .use(structureTables) + .use(cleanUnusedSpans) .use(structureReferences) .use(structureFootnotes) + .use(appendFigureAttributes) /* Assumes figures are
elements */ + .use(removeEmptyFigCaption) + .use(removeDescription) .use(rehypeFormat) .process(html); return String(result); @@ -62,12 +144,33 @@ const processHtml = async (html: string): Promise => { export const formatDriveData = async ( dataFromDrive: DriveData, - communitySlug: string + communitySlug: string, + pubId: string, + createVersions: boolean ): Promise => { const formattedPubHtml = await processHtml(dataFromDrive.pubHtml); + const formattedPubHtmlWithAssets = await processAssets(formattedPubHtml, pubId); + if (!createVersions) { + return { + pubHtml: String(formattedPubHtmlWithAssets), + pubDescription: "", + versions: [], + discussions: [], + }; + } + /* Check for a description in the most recent version */ + const latestRawVersion = dataFromDrive.versions.reduce((latest, version) => { + return new Date(version.timestamp) > new Date(latest.timestamp) ? version : latest; + }, dataFromDrive.versions[0]); + + const latestPubDescription = latestRawVersion + ? getDescription(latestRawVersion.html) + : getDescription(dataFromDrive.pubHtml); + + /* Align versions to releases in legacy data and process HTML */ const releases: any = dataFromDrive.legacyData?.releases || []; - const findDescription = (timestamp: string) => { + const findVersionDescription = (timestamp: string) => { const matchingRelease = releases.find((release: any) => { return release.createdAt === timestamp; }); @@ -82,7 +185,7 @@ export const formatDriveData = async ( const versions = dataFromDrive.versions.map((version) => { const { timestamp, html } = version; const outputVersion: any = { - [`${communitySlug}:description`]: findDescription(timestamp), + [`${communitySlug}:description`]: findVersionDescription(timestamp), [`${communitySlug}:publication-date`]: timestamp, [`${communitySlug}:content`]: html, }; @@ -126,6 +229,38 @@ export const formatDriveData = async ( : comment.commenter && comment.commenter.orcid ? `https://orcid.org/${comment.commenter.orcid}` : null; + const convertDiscussionContent = (content: any) => { + const traverse = (node: any) => { + if (node.type === "image") { + return { ...node, attrs: { ...node.attrs, src: node.attrs.url } }; + } + if (node.type === "file") { + return { + type: "paragraph", + content: [ + { + text: node.attrs.fileName, + type: "text", + marks: [ + { type: "link", attrs: { href: node.attrs.url } }, + ], + }, + ], + }; + } + if (node.content) { + return { ...node, content: node.content.map(traverse) }; + } + return node; + }; + return traverse(content); + }; + const prosemirrorToMarkdown = (content: any): string => { + const convertedContent = convertDiscussionContent(content); + const doc = Node.fromJSON(schema, convertedContent); + return defaultMarkdownSerializer.serialize(doc); + }; + const markdownContent = prosemirrorToMarkdown(comment.content); const commentObject: any = { id: comment.id, values: { @@ -133,7 +268,8 @@ export const formatDriveData = async ( index === 0 && discussion.anchors.length ? JSON.stringify(discussion.anchors[0]) : undefined, - [`${communitySlug}:content`]: comment.text, + // [`${communitySlug}:content`]: comment.text, + [`${communitySlug}:content`]: markdownContent, [`${communitySlug}:publication-date`]: comment.createdAt, [`${communitySlug}:full-name`]: commentAuthorName, [`${communitySlug}:orcid`]: commentAuthorORCID, @@ -161,7 +297,8 @@ export const formatDriveData = async ( const comments = discussions ? flattenComments(discussions) : []; const output = { - pubHtml: String(formattedPubHtml), + pubDescription: latestPubDescription, + pubHtml: String(formattedPubHtmlWithAssets), versions, discussions: comments, }; diff --git a/core/actions/googleDriveImport/gdocPlugins.test.ts b/core/actions/googleDriveImport/gdocPlugins.test.ts index c737a27942..bb9466aba5 100644 --- a/core/actions/googleDriveImport/gdocPlugins.test.ts +++ b/core/actions/googleDriveImport/gdocPlugins.test.ts @@ -4,8 +4,15 @@ import { expect, test } from "vitest"; import { logger } from "logger"; import { + appendFigureAttributes, basic, + cleanUnusedSpans, + formatFigureReferences, + formatLists, + getDescription, processLocalLinks, + removeDescription, + removeEmptyFigCaption, removeGoogleLinkForwards, removeVerboseFormatting, structureAnchors, @@ -21,6 +28,7 @@ import { structureInlineCode, structureInlineMath, structureReferences, + structureTables, structureVideos, tableToObjectArray, } from "./gdocPlugins"; @@ -73,6 +81,43 @@ test("Convert double table", async () => { expect(result).toStrictEqual(expectedOutput); }); +test("Convert vert table", async () => { + const inputNode = JSON.parse( + '{"type":"element","tagName":"table","properties":{},"children":[{"type":"element","tagName":"tbody","properties":{},"children":[{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Type"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Image"}]}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Id"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"n8r4ihxcrly"}]}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Source"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"https://resize-v3.pubpub.org/123"}]}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Alt Text"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"b","properties":{},"children":[{"type":"text","value":"123"}]}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Align"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"full"}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{},"children":[{"type":"text","value":"Size"}]}]}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"50"}]}]}]}]}]}' + ); + const expectedOutput = [ + { + type: "image", + id: "n8r4ihxcrly", + source: "https://resize-v3.pubpub.org/123", + alttext: "123", + align: "full", + size: "50", + }, + ]; + + const result = tableToObjectArray(inputNode); + + expect(result).toStrictEqual(expectedOutput); +}); + +test("Convert link-source table", async () => { + const inputNode = JSON.parse( + '{"type":"element","tagName":"table","children":[{"type":"element","tagName":"tbody","children":[{"type":"element","tagName":"tr","children":[{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"element","tagName":"span","children":[{"type":"text","value":"Type"}]}]}]},{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"text"},{"type":"element","tagName":"span","children":[{"type":"text","value":"Source"}]}]}]},{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"element","tagName":"span","children":[{"type":"text","value":"Static Image"}]}]}]}]},{"type":"element","tagName":"tr","children":[{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"element","tagName":"span","children":[{"type":"text","value":"Video"}]}]}]},{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"element","tagName":"span","children":[{"type":"element","tagName":"a","properties":{"href":"https://www.image-url.com"},"children":[{"type":"text","value":"image-filename.png"}]}]}]}]},{"type":"element","tagName":"td","children":[{"type":"element","tagName":"p","children":[{"type":"element","tagName":"span","children":[{"type":"element","tagName":"a","properties":{"href":"https://www.fallback-url.com"},"children":[{"type":"text","value":"fallback-filename.png"}]}]}]}]}]}]}]}' + ); + const expectedOutput = [ + { + source: "https://www.image-url.com", + type: "video", + staticimage: "https://www.fallback-url.com", + }, + ]; + + const result = tableToObjectArray(inputNode); + + expect(result).toStrictEqual(expectedOutput); +}); + test("Do Nothing", async () => { const inputHtml = '
Content
'; @@ -137,6 +182,8 @@ test("Structure Images", async () => {

Source

Caption

Alt Text

+

Align

+

Size

Image

@@ -144,6 +191,8 @@ test("Structure Images", async () => {

https://resize-v3.pubpub.org/123

With a caption. Bold

123

+

full

+

50

@@ -154,7 +203,7 @@ test("Structure Images", async () => { -
+
123

@@ -177,8 +226,153 @@ test("Structure Images", async () => { expect(trimAll(result)).toBe(trimAll(expectedOutputHtml)); }); +test("Structure Images - Vert Table", async () => { + const inputHtml = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Type

Image

Id

n8r4ihxcrly

Source

https://resize-v3.pubpub.org/123

Caption

With a caption. Bold

Alt Text

123

Align

full

Size

50

+ + + `; + const expectedOutputHtml = ` + + + +

+ 123 +
+

+ With a caption. + Bold +

+
+
+ + + `; -test("Structure Images", async () => { + const result = await rehype() + .use(structureImages) + .process(inputHtml) + .then((file) => String(file)) + .catch((error) => { + logger.error(error); + }); + + expect(trimAll(result)).toBe(trimAll(expectedOutputHtml)); +}); +test("Structure Images - DoubleVert Table", async () => { + const inputHtml = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Type

Image

Image

Id

n8r4ihxcrly

abr4ihxcrly

Source

https://resize-v3.pubpub.org/123

https://resize-v4.pubpub.org/123

Caption

With a caption. Bold

Alt Text

123

abc

Align

full

left

Size

50

75

+ + + `; + const expectedOutputHtml = ` + + + +
+ 123 +
+

+ With a caption. + Bold +

+
+
+
+ abc +

+
+ + + `; + + const result = await rehype() + .use(structureImages) + .process(inputHtml) + .then((file) => String(file)) + .catch((error) => { + logger.error(error); + }); + + expect(trimAll(result)).toBe(trimAll(expectedOutputHtml)); +}); + +test("Structure Videos", async () => { const inputHtml = ` @@ -191,6 +385,8 @@ test("Structure Images", async () => {

Source

Caption

Static Image

+

Align

+

Size

Video

@@ -198,6 +394,8 @@ test("Structure Images", async () => {

https://resize-v3.pubpub.org/123.mp4

With a caption. Bold

https://example.com +

full

+

50

@@ -208,7 +406,7 @@ test("Structure Images", async () => { -
+