diff --git a/client/package-lock.json b/client/package-lock.json index 8ab9475..76572fc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -29,7 +29,7 @@ "@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@roostorg/types": "^2.0.0", + "@roostorg/types": "^2.1.0", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", @@ -3146,9 +3146,9 @@ ] }, "node_modules/@roostorg/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@roostorg/types/-/types-2.0.0.tgz", - "integrity": "sha512-EyLecshJqiZdughsBgaJcR6nOmZ6cT5ioNeRB1Cif6b0ZrXc9LfBirXd2FPZ1qzxxIjgMUOA1HSxfFVThMRZlw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@roostorg/types/-/types-2.1.0.tgz", + "integrity": "sha512-nxPXjtAdf7ut/X/NYgbDl39r2wDKiO5vEeLPIHjV8MjDQ1/O5CbgJDyi1jvchMnIJW/AKskdask/Yu0J2JuICQ==", "license": "ISC", "dependencies": { "date-fns": "^2.29.3", diff --git a/client/package.json b/client/package.json index 99e149e..aea1649 100644 --- a/client/package.json +++ b/client/package.json @@ -36,7 +36,7 @@ "@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@roostorg/types": "^2.0.0", + "@roostorg/types": "^2.1.0", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", diff --git a/client/src/graphql/generated.ts b/client/src/graphql/generated.ts index d214bc2..e5b5c66 100644 --- a/client/src/graphql/generated.ts +++ b/client/src/graphql/generated.ts @@ -620,6 +620,7 @@ export type GQLContentSchemaFieldRoles = { readonly createdAt?: Maybe; readonly creatorId?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; readonly parentId?: Maybe; readonly threadId?: Maybe; @@ -629,6 +630,7 @@ export type GQLContentSchemaFieldRolesInput = { readonly createdAt?: InputMaybe; readonly creatorId?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; readonly parentId?: InputMaybe; readonly threadId?: InputMaybe; @@ -1273,6 +1275,7 @@ export const GQLFieldType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Map: 'MAP', Number: 'NUMBER', PolicyId: 'POLICY_ID', @@ -4073,6 +4076,7 @@ export const GQLScalarType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Number: 'NUMBER', PolicyId: 'POLICY_ID', RelatedItem: 'RELATED_ITEM', @@ -4212,6 +4216,7 @@ export const GQLSignalInputType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Number: 'NUMBER', PolicyId: 'POLICY_ID', RelatedItem: 'RELATED_ITEM', @@ -4471,6 +4476,7 @@ export type GQLThreadSchemaFieldRoles = { readonly createdAt?: Maybe; readonly creatorId?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; }; @@ -4478,6 +4484,7 @@ export type GQLThreadSchemaFieldRolesInput = { readonly createdAt?: InputMaybe; readonly creatorId?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; }; @@ -4949,6 +4956,7 @@ export type GQLUserSchemaFieldRoles = { readonly backgroundImage?: Maybe; readonly createdAt?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; readonly profileIcon?: Maybe; }; @@ -4957,6 +4965,7 @@ export type GQLUserSchemaFieldRolesInput = { readonly backgroundImage?: InputMaybe; readonly createdAt?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; readonly profileIcon?: InputMaybe; }; @@ -6314,6 +6323,7 @@ export type GQLInvestigationItemTypesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6371,6 +6381,7 @@ export type GQLInvestigationItemTypesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6429,6 +6440,7 @@ export type GQLInvestigationItemTypesQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6595,6 +6607,7 @@ export type GQLGetOrgDataQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6652,6 +6665,7 @@ export type GQLGetOrgDataQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6710,6 +6724,7 @@ export type GQLGetOrgDataQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -6859,6 +6874,7 @@ export type GQLGetItemsWithIdQuery = { readonly threadId?: string | null; readonly createdAt?: string | null; readonly creatorId?: string | null; + readonly ipAddress?: string | null; }; }; } @@ -6891,6 +6907,7 @@ export type GQLGetItemsWithIdQuery = { readonly displayName?: string | null; readonly createdAt?: string | null; readonly creatorId?: string | null; + readonly ipAddress?: string | null; }; }; } @@ -6924,6 +6941,7 @@ export type GQLGetItemsWithIdQuery = { readonly displayName?: string | null; readonly createdAt?: string | null; readonly profileIcon?: string | null; + readonly ipAddress?: string | null; }; }; }; @@ -7218,6 +7236,7 @@ export type GQLItemTypeQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7275,6 +7294,7 @@ export type GQLItemTypeQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7333,6 +7353,7 @@ export type GQLItemTypeQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7545,6 +7566,7 @@ type GQLItemFieldsContentItemFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7610,6 +7632,7 @@ type GQLItemFieldsThreadItemFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7676,6 +7699,7 @@ type GQLItemFieldsUserItemFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7749,6 +7773,7 @@ export type GQLItemTypesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7806,6 +7831,7 @@ export type GQLItemTypesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -7865,6 +7891,7 @@ export type GQLItemTypesQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8046,6 +8073,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8112,6 +8140,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8203,6 +8232,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8269,6 +8299,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8335,6 +8366,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8443,6 +8475,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8514,6 +8547,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8578,6 +8612,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8643,6 +8678,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8745,6 +8781,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8833,6 +8870,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -8945,6 +8983,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9011,6 +9050,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9101,6 +9141,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9167,6 +9208,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9238,6 +9280,7 @@ export type GQLGetDecidedJobFromJobIdQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9717,6 +9760,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9786,6 +9830,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9854,6 +9899,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9921,6 +9967,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -9988,6 +10035,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10056,6 +10104,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10124,6 +10173,7 @@ export type GQLManualReviewQueueJobsPreviewQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10640,6 +10690,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10706,6 +10757,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10797,6 +10849,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10863,6 +10916,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -10929,6 +10983,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11037,6 +11092,7 @@ export type GQLGetDecidedJobQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11108,6 +11164,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11172,6 +11229,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11237,6 +11295,7 @@ export type GQLGetDecidedJobQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11339,6 +11398,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11427,6 +11487,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11539,6 +11600,7 @@ export type GQLGetDecidedJobQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11605,6 +11667,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11695,6 +11758,7 @@ export type GQLGetDecidedJobQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11761,6 +11825,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11832,6 +11897,7 @@ export type GQLGetDecidedJobQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -11990,6 +12056,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12047,6 +12114,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12105,6 +12173,7 @@ export type GQLManualReviewJobInfoQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12313,6 +12382,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12379,6 +12449,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12470,6 +12541,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12536,6 +12608,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12602,6 +12675,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12710,6 +12784,7 @@ export type GQLManualReviewJobInfoQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12781,6 +12856,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12845,6 +12921,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -12910,6 +12987,7 @@ export type GQLManualReviewJobInfoQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13012,6 +13090,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13100,6 +13179,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13212,6 +13292,7 @@ export type GQLManualReviewJobInfoQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13278,6 +13359,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13368,6 +13450,7 @@ export type GQLManualReviewJobInfoQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13434,6 +13517,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13505,6 +13589,7 @@ export type GQLManualReviewJobInfoQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13640,6 +13725,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13706,6 +13792,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13797,6 +13884,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13863,6 +13951,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -13929,6 +14018,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14037,6 +14127,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14108,6 +14199,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14172,6 +14264,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14237,6 +14330,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14339,6 +14433,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14427,6 +14522,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14539,6 +14635,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14605,6 +14702,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14695,6 +14793,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14761,6 +14860,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -14832,6 +14932,7 @@ export type GQLDequeueManualReviewJobMutation = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15012,6 +15113,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15078,6 +15180,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15169,6 +15272,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15235,6 +15339,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15301,6 +15406,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15409,6 +15515,7 @@ export type GQLJobFieldsFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15480,6 +15587,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15544,6 +15652,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15609,6 +15718,7 @@ export type GQLJobFieldsFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15711,6 +15821,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15799,6 +15910,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15911,6 +16023,7 @@ export type GQLJobFieldsFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -15977,6 +16090,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16067,6 +16181,7 @@ export type GQLJobFieldsFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16133,6 +16248,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16204,6 +16320,7 @@ export type GQLJobFieldsFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16701,6 +16818,7 @@ export type GQLGetMoreInfoForPartialItemsQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16765,6 +16883,7 @@ export type GQLGetMoreInfoForPartialItemsQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -16830,6 +16949,7 @@ export type GQLGetMoreInfoForPartialItemsQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17173,6 +17293,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17230,6 +17351,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17288,6 +17410,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17325,6 +17448,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17382,6 +17506,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17440,6 +17565,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17673,6 +17799,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17730,6 +17857,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -17788,6 +17916,7 @@ export type GQLManualReviewQueueRoutingRulesQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21037,6 +21166,7 @@ export type GQLReportingRuleQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21094,6 +21224,7 @@ export type GQLReportingRuleQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21152,6 +21283,7 @@ export type GQLReportingRuleQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21489,6 +21621,7 @@ export type GQLReportingRuleFormOrgDataQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21546,6 +21679,7 @@ export type GQLReportingRuleFormOrgDataQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21604,6 +21738,7 @@ export type GQLReportingRuleFormOrgDataQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21848,6 +21983,7 @@ type GQLItemTypeFragmentContentItemTypeFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21906,6 +22042,7 @@ type GQLItemTypeFragmentThreadItemTypeFragment = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -21965,6 +22102,7 @@ type GQLItemTypeFragmentUserItemTypeFragment = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -22880,6 +23018,7 @@ export type GQLRuleQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -22937,6 +23076,7 @@ export type GQLRuleQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -22995,6 +23135,7 @@ export type GQLRuleQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -23704,6 +23845,7 @@ export type GQLContentRuleFormConfigQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -23761,6 +23903,7 @@ export type GQLContentRuleFormConfigQuery = { readonly createdAt?: string | null; readonly creatorId?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -23819,6 +23962,7 @@ export type GQLContentRuleFormConfigQuery = { readonly profileIcon?: string | null; readonly backgroundImage?: string | null; readonly isDeleted?: string | null; + readonly ipAddress?: string | null; }; readonly baseFields: ReadonlyArray<{ readonly __typename: 'BaseField'; @@ -24587,6 +24731,7 @@ export const GQLItemTypeFragmentFragmentDoc = gql` profileIcon backgroundImage isDeleted + ipAddress } } ... on ContentItemType { @@ -24597,6 +24742,7 @@ export const GQLItemTypeFragmentFragmentDoc = gql` createdAt creatorId isDeleted + ipAddress } } ... on ThreadItemType { @@ -24605,6 +24751,7 @@ export const GQLItemTypeFragmentFragmentDoc = gql` createdAt creatorId isDeleted + ipAddress } } } @@ -24624,6 +24771,7 @@ export const GQLItemFieldsFragmentDoc = gql` profileIcon backgroundImage isDeleted + ipAddress } } ... on ContentItemType { @@ -24634,6 +24782,7 @@ export const GQLItemFieldsFragmentDoc = gql` createdAt creatorId isDeleted + ipAddress } } ... on ThreadItemType { @@ -24642,6 +24791,7 @@ export const GQLItemFieldsFragmentDoc = gql` createdAt creatorId isDeleted + ipAddress } } } @@ -29914,6 +30064,7 @@ export const GQLGetItemsWithIdDocument = gql` threadId createdAt creatorId + ipAddress } } ... on UserItemType { @@ -29921,6 +30072,7 @@ export const GQLGetItemsWithIdDocument = gql` displayName createdAt profileIcon + ipAddress } } ... on ThreadItemType { @@ -29928,6 +30080,7 @@ export const GQLGetItemsWithIdDocument = gql` displayName createdAt creatorId + ipAddress } } } diff --git a/client/src/models/signal.ts b/client/src/models/signal.ts index 890a2a7..d0e77e0 100644 --- a/client/src/models/signal.ts +++ b/client/src/models/signal.ts @@ -124,6 +124,7 @@ export function outputTypeToComparators(outputType: GQLSignalOutputType) { ]; case GQLScalarType.Url: case GQLScalarType.String: + case GQLScalarType.IpAddress: return outputType.__typename === 'EnumSignalOutputType' && outputType.ordered ? orderedComparators diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx index d6d4b11..6e599f6 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx @@ -97,6 +97,7 @@ gql` threadId createdAt creatorId + ipAddress } } ... on UserItemType { @@ -104,6 +105,7 @@ gql` displayName createdAt profileIcon + ipAddress } } ... on ThreadItemType { @@ -111,6 +113,7 @@ gql` displayName createdAt creatorId + ipAddress } } } diff --git a/client/src/webpages/dashboard/item_types/ItemTypeForm.tsx b/client/src/webpages/dashboard/item_types/ItemTypeForm.tsx index 1709160..17c4811 100644 --- a/client/src/webpages/dashboard/item_types/ItemTypeForm.tsx +++ b/client/src/webpages/dashboard/item_types/ItemTypeForm.tsx @@ -642,15 +642,15 @@ export default function ItemTypeForm() { !canEditItemTypes ? `To edit ${readableKind} Types, ask your organization's admin to upgrade your role to Admin.` : customFields.every((it) => it.name.length === 0) - ? 'Please add at least one field with a name.' - : customFields.some( - (it) => it.role === SchemaFieldRoles.PARENT_ID, - ) && - !customFields.some( - (it) => it.role === SchemaFieldRoles.THREAD_ID, - ) - ? `Content items with a Parent field must also have a Thread field.` - : `Content items with a Thread field must have a Created-At field.` + ? 'Please add at least one field with a name.' + : customFields.some( + (it) => it.role === SchemaFieldRoles.PARENT_ID, + ) && + !customFields.some( + (it) => it.role === SchemaFieldRoles.THREAD_ID, + ) + ? `Content items with a Parent field must also have a Thread field.` + : `Content items with a Thread field must have a Created-At field.` } disabledTooltipPlacement="bottomLeft" onClick={async () => @@ -766,6 +766,7 @@ function availableRolesForItemKind(kind: ItemTypeKind): SchemaFieldRoles[] { SchemaFieldRoles.DISPLAY_NAME, SchemaFieldRoles.PARENT_ID, SchemaFieldRoles.IS_DELETED, + SchemaFieldRoles.IP_ADDRESS, SchemaFieldRoles.NONE, ]; case 'THREAD': @@ -774,6 +775,7 @@ function availableRolesForItemKind(kind: ItemTypeKind): SchemaFieldRoles[] { SchemaFieldRoles.DISPLAY_NAME, SchemaFieldRoles.CREATOR_ID, SchemaFieldRoles.IS_DELETED, + SchemaFieldRoles.IP_ADDRESS, SchemaFieldRoles.NONE, ]; case 'USER': @@ -783,6 +785,7 @@ function availableRolesForItemKind(kind: ItemTypeKind): SchemaFieldRoles[] { SchemaFieldRoles.PROFILE_ICON, SchemaFieldRoles.BACKGROUND_IMAGE, SchemaFieldRoles.IS_DELETED, + SchemaFieldRoles.IP_ADDRESS, SchemaFieldRoles.NONE, ]; } diff --git a/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx b/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx index 5542260..71406db 100644 --- a/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx +++ b/client/src/webpages/dashboard/item_types/ItemTypesDashboard.tsx @@ -53,6 +53,7 @@ export const ITEM_FRAGMENT = gql` profileIcon backgroundImage isDeleted + ipAddress } } ... on ContentItemType { @@ -63,6 +64,7 @@ export const ITEM_FRAGMENT = gql` createdAt creatorId isDeleted + ipAddress } } ... on ThreadItemType { @@ -71,6 +73,7 @@ export const ITEM_FRAGMENT = gql` createdAt creatorId isDeleted + ipAddress } } } diff --git a/client/src/webpages/dashboard/item_types/itemTypeUtils.ts b/client/src/webpages/dashboard/item_types/itemTypeUtils.ts index b5a29b0..c43c0ab 100644 --- a/client/src/webpages/dashboard/item_types/itemTypeUtils.ts +++ b/client/src/webpages/dashboard/item_types/itemTypeUtils.ts @@ -20,10 +20,10 @@ export type FieldRoles = WithoutTypename< T extends 'CONTENT' ? GQLContentSchemaFieldRoles : T extends 'THREAD' - ? GQLThreadSchemaFieldRoles - : T extends 'USER' - ? GQLUserSchemaFieldRoles - : never + ? GQLThreadSchemaFieldRoles + : T extends 'USER' + ? GQLUserSchemaFieldRoles + : never >; export enum SchemaFieldRoles { @@ -36,6 +36,7 @@ export enum SchemaFieldRoles { DISPLAY_NAME = 'displayName', BACKGROUND_IMAGE = 'backgroundImage', IS_DELETED = 'isDeleted', + IP_ADDRESS = 'ipAddress', } export const schemaFieldRolesFieldTypes = { @@ -47,6 +48,7 @@ export const schemaFieldRolesFieldTypes = { [SchemaFieldRoles.DISPLAY_NAME]: GQLScalarType.String, [SchemaFieldRoles.BACKGROUND_IMAGE]: GQLScalarType.Image, [SchemaFieldRoles.IS_DELETED]: GQLScalarType.Boolean, + [SchemaFieldRoles.IP_ADDRESS]: GQLScalarType.IpAddress, } satisfies Omit< { [key in SchemaFieldRoles]: GQLScalarType }, SchemaFieldRoles.NONE @@ -73,6 +75,8 @@ export function getDisplayStringForRole( return 'Cover Photo'; case SchemaFieldRoles.IS_DELETED: return 'Is Deleted'; + case SchemaFieldRoles.IP_ADDRESS: + return 'IP Address'; case SchemaFieldRoles.NONE: return 'None'; } @@ -219,6 +223,8 @@ export function generateFakeScalarFieldValue(fieldType: ScalarType) { return 'Lorem ipsum dolor'; case 'URL': return `https://url.com/some-path/${Math.floor(100 * Math.random())}`; + case 'IP_ADDRESS': + return `192.0.2.${Math.floor(255 * Math.random())}`; } } diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx index 3158051..45c8606 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobFieldsComponent.tsx @@ -145,6 +145,7 @@ function TableRowComponent(props: { case 'ID': case 'NUMBER': case 'POLICY_ID': + case 'IP_ADDRESS': case 'STRING': { return (
@@ -176,7 +177,7 @@ function TableRowComponent(props: { if (url == null) { return ; } - + // Extract matched banks if available const matchedBanks = (value as any)?.matchedBanks; const hasMatches = Array.isArray(matchedBanks) && matchedBanks.length > 0; @@ -194,8 +195,8 @@ function TableRowComponent(props: { blurStrength: unblurAllMedia ? (0 as const) : safetySettings?.moderatorSafetyBlurLevel - ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) - : (2 as const), + ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) + : (2 as const), grayscale: safetySettings?.moderatorSafetyGrayscale ?? false, }} /> @@ -232,8 +233,8 @@ function TableRowComponent(props: { blurStrength: unblurAllMedia ? (0 as const) : safetySettings?.moderatorSafetyBlurLevel - ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) - : (2 as const), + ? (safetySettings.moderatorSafetyBlurLevel as BlurStrength) + : (2 as const), maxWidth: maxWidthVideo, maxHeight: maxHeightVideo, muted: safetySettings?.moderatorSafetyMuteVideo ?? true, @@ -350,6 +351,7 @@ function FieldComponent(props: { case 'RELATED_ITEM': case 'URL': case 'POLICY_ID': + case 'IP_ADDRESS': case 'DATETIME': return (
@@ -405,6 +407,7 @@ function ContainerComponent(props: { case 'USER_ID': case 'DATETIME': case 'POLICY_ID': + case 'IP_ADDRESS': return true; case 'AUDIO': case 'IMAGE': @@ -445,6 +448,7 @@ function ContainerComponent(props: { case 'RELATED_ITEM': case 'URL': case 'POLICY_ID': + case 'IP_ADDRESS': case 'VIDEO': { throw Error('Cannot call container component with scalar field'); } diff --git a/client/src/webpages/dashboard/rules/info/RuleTestModal.tsx b/client/src/webpages/dashboard/rules/info/RuleTestModal.tsx index 47cf310..88db331 100644 --- a/client/src/webpages/dashboard/rules/info/RuleTestModal.tsx +++ b/client/src/webpages/dashboard/rules/info/RuleTestModal.tsx @@ -136,6 +136,8 @@ export default function RuleTestModal(props: { return 'https://test.com'; case 'USER_ID': return 'user-id'; + case 'IP_ADDRESS': + return '192.0.2.1'; case 'ARRAY': return `${getPlaceholder( containerValueScalarType!, diff --git a/client/src/webpages/dashboard/rules/info/insights/RuleInsightsSamplesTable.tsx b/client/src/webpages/dashboard/rules/info/insights/RuleInsightsSamplesTable.tsx index c912d24..e0f5421 100644 --- a/client/src/webpages/dashboard/rules/info/insights/RuleInsightsSamplesTable.tsx +++ b/client/src/webpages/dashboard/rules/info/insights/RuleInsightsSamplesTable.tsx @@ -763,6 +763,7 @@ export function getStringFromContent( case GQLFieldType.Array: case GQLFieldType.Map: case GQLFieldType.PolicyId: + case GQLFieldType.IpAddress: return content.toString(); case GQLFieldType.Image: case GQLFieldType.Video: diff --git a/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx b/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx index 62918ea..b61c2ea 100644 --- a/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx @@ -172,6 +172,7 @@ export const ITEM_TYPE_FRAGMENT = gql` profileIcon backgroundImage isDeleted + ipAddress } } ... on ContentItemType { @@ -182,6 +183,7 @@ export const ITEM_TYPE_FRAGMENT = gql` createdAt creatorId isDeleted + ipAddress } } ... on ThreadItemType { @@ -190,6 +192,7 @@ export const ITEM_TYPE_FRAGMENT = gql` createdAt creatorId isDeleted + ipAddress } } } @@ -639,7 +642,6 @@ export default function RuleForm() { const [state, dispatch] = useReducer(reducer, initialState); const [form] = useForm(); - const onDeleteRule = (id: string) => { deleteRule({ @@ -915,7 +917,9 @@ export default function RuleForm() { policyIds: state.policyIds, tags: state.tags, maxDailyActions: state.maxDailyActions, - expirationTime: state.expirationTime ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') : undefined, + expirationTime: state.expirationTime + ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') + : undefined, }, }, refetchQueries: [namedOperations.Query.Rules], @@ -939,7 +943,9 @@ export default function RuleForm() { policyIds: state.policyIds, tags: state.tags, maxDailyActions: state.maxDailyActions, - expirationTime: state.expirationTime ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') : undefined, + expirationTime: state.expirationTime + ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') + : undefined, }, }, refetchQueries: [ @@ -962,7 +968,9 @@ export default function RuleForm() { policyIds: state.policyIds, tags: state.tags, maxDailyActions: state.maxDailyActions, - expirationTime: state.expirationTime ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') : undefined, + expirationTime: state.expirationTime + ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') + : undefined, }, }, refetchQueries: [namedOperations.Query.Rules], @@ -985,7 +993,9 @@ export default function RuleForm() { policyIds: state.policyIds, tags: state.tags, maxDailyActions: state.maxDailyActions, - expirationTime: state.expirationTime ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') : undefined, + expirationTime: state.expirationTime + ? format(state.expirationTime, 'yyyy-MM-dd HH:mm') + : undefined, }, }, refetchQueries: [ @@ -1647,10 +1657,17 @@ export default function RuleForm() { /> {!state.expirationEnabled &&
} {state.expirationEnabled && ( -
+
dispatch({ @@ -1716,14 +1733,14 @@ export default function RuleForm() { !canEditLiveRules ? "To edit Live rules, ask your organization's admin to upgrade your role to Rules Manager or Admin." : !canEditNonLiveRules - ? "To edit rules, ask your organization's admin to upgrade your role to Rules Manager or Admin." - : !actionsSelected - ? 'Please select at least one action.' - : hasInvalidThreshold - ? 'At least one threshold has an invalid input.' - : !conditionsValid - ? 'This rule only has user-based conditions, but rules must contain at least one content-based condition.' - : undefined + ? "To edit rules, ask your organization's admin to upgrade your role to Rules Manager or Admin." + : !actionsSelected + ? 'Please select at least one action.' + : hasInvalidThreshold + ? 'At least one threshold has an invalid input.' + : !conditionsValid + ? 'This rule only has user-based conditions, but rules must contain at least one content-based condition.' + : undefined } disabledTooltipPlacement="bottomLeft" /> @@ -1884,8 +1901,8 @@ export default function RuleForm() { ? 'Create User Rule' : 'Create Rule' : isUserRule - ? 'Update User Rule' - : 'Update Rule'; + ? 'Update User Rule' + : 'Update Rule'; })()} // topRightButton={ruleTypeSelector} /> diff --git a/db/src/scripts/api-server-pg/2026.05.21T16.50.30.add_ip_address_field_role_to_item_types.sql b/db/src/scripts/api-server-pg/2026.05.21T16.50.30.add_ip_address_field_role_to_item_types.sql new file mode 100644 index 0000000..cf08a3e --- /dev/null +++ b/db/src/scripts/api-server-pg/2026.05.21T16.50.30.add_ip_address_field_role_to_item_types.sql @@ -0,0 +1,134 @@ +-- Issue #468: add an `ipAddress` schema field role to item types so adopters +-- can tag a string IP-address field on user / thread / content items and have +-- Coop surface it as an identifier in moderation flows. +-- +-- `ip_address_field` lives on `public.item_types` and its temporal mirror +-- `public.item_types_history`. The materialized view `item_type_versions` +-- joins both tables, so we have to drop and recreate it (with its indexes +-- and the `item_type_latest_versions` view that depends on it) so the new +-- column is selectable. Postgres handles cascading via DROP ... CASCADE. + +BEGIN; + +ALTER TABLE public.item_types + ADD COLUMN ip_address_field character varying(255); + +ALTER TABLE public.item_types_history + ADD COLUMN ip_address_field character varying(255); + +-- Mirrors the per-role clauses in `valid_field_role_field_type`, but as a +-- standalone constraint so we don't touch the existing one. +ALTER TABLE public.item_types + ADD CONSTRAINT valid_ip_address_field_field_type CHECK ( + (ip_address_field IS NULL) + OR jsonb_path_exists( + (array_to_json(fields))::jsonb, + '$[*]?(@."name" == $"name" && @."type" == "IP_ADDRESS")'::jsonpath, + jsonb_build_object('name', ip_address_field) + ) + ); + +-- CASCADE drops the four indexes on item_type_versions and the +-- item_type_latest_versions view; both are recreated below. +DROP MATERIALIZED VIEW public.item_type_versions CASCADE; + +CREATE MATERIALIZED VIEW public.item_type_versions AS +WITH item_type_versions AS ( + SELECT + item_types.id, + item_types.name, + item_types.description, + item_types.fields, + item_types.org_id, + item_types.sys_period, + item_types.kind, + item_types.display_name_field, + item_types.creator_id_field, + item_types.thread_id_field, + item_types.parent_id_field, + item_types.created_at_field, + item_types.is_deleted_field, + item_types.profile_icon_field, + item_types.background_image_field, + item_types.ip_address_field, + item_types.is_default_user + FROM public.item_types + UNION ALL + SELECT + item_types_history.id, + item_types_history.name, + item_types_history.description, + item_types_history.fields, + item_types_history.org_id, + item_types_history.sys_period, + item_types_history.kind, + item_types_history.display_name_field, + item_types_history.creator_id_field, + item_types_history.thread_id_field, + item_types_history.parent_id_field, + item_types_history.created_at_field, + item_types_history.is_deleted_field, + item_types_history.profile_icon_field, + item_types_history.background_image_field, + item_types_history.ip_address_field, + item_types_history.is_default_user + FROM public.item_types_history +), item_type_max_period_starts AS ( + SELECT + item_type_versions_1.id, + max(lower(item_type_versions_1.sys_period)) AS max_period_start + FROM item_type_versions item_type_versions_1 + GROUP BY item_type_versions_1.id +) +SELECT + item_type_versions.id, + item_type_versions.name, + item_type_versions.description, + item_type_versions.fields, + item_type_versions.org_id, + item_type_versions.kind, + item_type_versions.display_name_field, + item_type_versions.creator_id_field, + item_type_versions.thread_id_field, + item_type_versions.parent_id_field, + item_type_versions.created_at_field, + item_type_versions.is_deleted_field, + item_type_versions.profile_icon_field, + item_type_versions.background_image_field, + item_type_versions.ip_address_field, + item_type_versions.is_default_user, + lower(item_type_versions.sys_period) AS version, + ( + (item_type_max_period_starts.max_period_start = lower(item_type_versions.sys_period)) + AND upper_inf(item_type_versions.sys_period) + ) AS is_current +FROM item_type_versions +JOIN item_type_max_period_starts + ON ((item_type_max_period_starts.id)::text = (item_type_versions.id)::text) +WITH DATA; + +ALTER TABLE public.item_type_versions OWNER TO CURRENT_USER; + +CREATE INDEX item_type_versions_id_idx + ON public.item_type_versions USING btree (id); + +CREATE UNIQUE INDEX item_type_versions_id_is_current_idx + ON public.item_type_versions USING btree (id, is_current) + WHERE (is_current = true); + +CREATE INDEX item_type_versions_is_current_idx + ON public.item_type_versions USING btree (is_current); + +CREATE INDEX item_type_versions_version_idx + ON public.item_type_versions USING btree (version); + +CREATE VIEW public.item_type_latest_versions AS + SELECT + item_type_versions.id AS item_type_id, + to_char((item_type_versions.version AT TIME ZONE 'UTC'::text), 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'::text) AS version + FROM public.item_type_versions + WHERE (item_type_versions.is_current = true); + +ALTER TABLE public.item_type_latest_versions OWNER TO CURRENT_USER; + +COMMIT; diff --git a/server/graphql/generated.ts b/server/graphql/generated.ts index e5f39ed..63f3590 100644 --- a/server/graphql/generated.ts +++ b/server/graphql/generated.ts @@ -687,6 +687,7 @@ export type GQLContentSchemaFieldRoles = { readonly createdAt?: Maybe; readonly creatorId?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; readonly parentId?: Maybe; readonly threadId?: Maybe; @@ -696,6 +697,7 @@ export type GQLContentSchemaFieldRolesInput = { readonly createdAt?: InputMaybe; readonly creatorId?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; readonly parentId?: InputMaybe; readonly threadId?: InputMaybe; @@ -1340,6 +1342,7 @@ export const GQLFieldType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Map: 'MAP', Number: 'NUMBER', PolicyId: 'POLICY_ID', @@ -4140,6 +4143,7 @@ export const GQLScalarType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Number: 'NUMBER', PolicyId: 'POLICY_ID', RelatedItem: 'RELATED_ITEM', @@ -4279,6 +4283,7 @@ export const GQLSignalInputType = { Geohash: 'GEOHASH', Id: 'ID', Image: 'IMAGE', + IpAddress: 'IP_ADDRESS', Number: 'NUMBER', PolicyId: 'POLICY_ID', RelatedItem: 'RELATED_ITEM', @@ -4538,6 +4543,7 @@ export type GQLThreadSchemaFieldRoles = { readonly createdAt?: Maybe; readonly creatorId?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; }; @@ -4545,6 +4551,7 @@ export type GQLThreadSchemaFieldRolesInput = { readonly createdAt?: InputMaybe; readonly creatorId?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; }; @@ -5016,6 +5023,7 @@ export type GQLUserSchemaFieldRoles = { readonly backgroundImage?: Maybe; readonly createdAt?: Maybe; readonly displayName?: Maybe; + readonly ipAddress?: Maybe; readonly isDeleted?: Maybe; readonly profileIcon?: Maybe; }; @@ -5024,6 +5032,7 @@ export type GQLUserSchemaFieldRolesInput = { readonly backgroundImage?: InputMaybe; readonly createdAt?: InputMaybe; readonly displayName?: InputMaybe; + readonly ipAddress?: InputMaybe; readonly isDeleted?: InputMaybe; readonly profileIcon?: InputMaybe; }; @@ -8004,6 +8013,11 @@ export type GQLContentSchemaFieldRolesResolvers< ParentType, ContextType >; + ipAddress?: Resolver< + Maybe, + ParentType, + ContextType + >; isDeleted?: Resolver< Maybe, ParentType, @@ -13837,6 +13851,11 @@ export type GQLThreadSchemaFieldRolesResolvers< ParentType, ContextType >; + ipAddress?: Resolver< + Maybe, + ParentType, + ContextType + >; isDeleted?: Resolver< Maybe, ParentType, @@ -14484,6 +14503,11 @@ export type GQLUserSchemaFieldRolesResolvers< ParentType, ContextType >; + ipAddress?: Resolver< + Maybe, + ParentType, + ContextType + >; isDeleted?: Resolver< Maybe, ParentType, diff --git a/server/graphql/modules/itemType.ts b/server/graphql/modules/itemType.ts index 3ceb78d..458ebc0 100644 --- a/server/graphql/modules/itemType.ts +++ b/server/graphql/modules/itemType.ts @@ -35,8 +35,8 @@ import { } from '../generated.js'; import { type Context } from '../resolvers.js'; import { formatItemSubmissionForGQL } from '../types.js'; -import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; import { unauthenticatedError } from '../utils/errors.js'; +import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; export type ItemTypeResolversParentType = ItemTypeT | ItemTypeSelector; export type ThreadItemTypeResolversParentType = @@ -219,6 +219,7 @@ const typeDefs = /* GraphQL */ ` profileIcon: String backgroundImage: String isDeleted: String + ipAddress: String } type ThreadSchemaFieldRoles { @@ -226,6 +227,7 @@ const typeDefs = /* GraphQL */ ` createdAt: String creatorId: String isDeleted: String + ipAddress: String } type ContentSchemaFieldRoles { @@ -235,10 +237,11 @@ const typeDefs = /* GraphQL */ ` createdAt: String creatorId: String isDeleted: String + ipAddress: String } union SchemaFieldRoles = - ContentSchemaFieldRoles + | ContentSchemaFieldRoles | ThreadSchemaFieldRoles | UserSchemaFieldRoles @@ -279,6 +282,7 @@ const typeDefs = /* GraphQL */ ` profileIcon: String backgroundImage: String isDeleted: String + ipAddress: String } input ThreadSchemaFieldRolesInput { @@ -286,6 +290,7 @@ const typeDefs = /* GraphQL */ ` createdAt: String creatorId: String isDeleted: String + ipAddress: String } input ContentSchemaFieldRolesInput { @@ -295,6 +300,7 @@ const typeDefs = /* GraphQL */ ` createdAt: String creatorId: String isDeleted: String + ipAddress: String } input CreateUserItemTypeInput { @@ -398,21 +404,21 @@ const typeDefs = /* GraphQL */ ` # Changed to not clash with existing response union MutateContentItemTypeResponse = - MutateContentTypeSuccessResponse + | MutateContentTypeSuccessResponse | ItemTypeNameAlreadyExistsError union MutateUserItemTypeResponse = - MutateUserTypeSuccessResponse + | MutateUserTypeSuccessResponse | ItemTypeNameAlreadyExistsError union MutateThreadItemTypeResponse = - MutateThreadTypeSuccessResponse + | MutateThreadTypeSuccessResponse | ItemTypeNameAlreadyExistsError union PartialItemsResponse = - PartialItemsSuccessResponse + | PartialItemsSuccessResponse | PartialItemsMissingEndpointError | PartialItemsEndpointResponseError | PartialItemsInvalidResponseError union DeleteItemTypeResponse = - DeleteItemTypeSuccessResponse + | DeleteItemTypeSuccessResponse | CannotDeleteDefaultUserError type Query { @@ -1079,9 +1085,9 @@ function isValidField(it: GQLFieldInput): it is Field { (containerType === ContainerTypes.MAP ? keyScalarType !== null : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - containerType === ContainerTypes.ARRAY - ? keyScalarType === null - : assertUnreachable(containerType)); + containerType === ContainerTypes.ARRAY + ? keyScalarType === null + : assertUnreachable(containerType)); return isValidContainerType; } diff --git a/server/graphql/schema.ts b/server/graphql/schema.ts index 7834c39..b8ee660 100644 --- a/server/graphql/schema.ts +++ b/server/graphql/schema.ts @@ -64,6 +64,7 @@ const typeDefs = /* GraphQL */ ` RELATED_ITEM URL POLICY_ID + IP_ADDRESS } # This is equivalent to ScalarType, but with 'FULL_ITEM' added @@ -82,6 +83,7 @@ const typeDefs = /* GraphQL */ ` URL FULL_ITEM POLICY_ID + IP_ADDRESS } # !! IMPORTANT: when you add a value here, also add it to FieldType !! @@ -106,6 +108,7 @@ const typeDefs = /* GraphQL */ ` RELATED_ITEM URL POLICY_ID + IP_ADDRESS } enum Language { @@ -331,7 +334,7 @@ const typeDefs = /* GraphQL */ ` } union InviteUserTokenResponse = - InviteUserTokenSuccessResponse + | InviteUserTokenSuccessResponse | InviteUserTokenExpiredError | InviteUserTokenMissingError diff --git a/server/package-lock.json b/server/package-lock.json index c09c434..407bbf0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -25,7 +25,7 @@ "@opentelemetry/api": "^1.8.0", "@opentelemetry/semantic-conventions": "^1.22.0", "@roostorg/coop-integration-example": "^2.0.0", - "@roostorg/types": "^2.0.0", + "@roostorg/types": "^2.1.0", "@sendgrid/mail": "^8.1.6", "@stdlib/stats-binomial-test": "^0.0.7", "@total-typescript/ts-reset": "^0.3.7", @@ -41,6 +41,7 @@ "date-fns": "^2.30.0", "express": "^5.1.0", "express-session": "^1.17.2", + "fast-check": "^4.6.0", "form-data": "^4.0.0", "form-data-encoder": "^4.0.2", "formdata-node": "^6.0.3", @@ -120,7 +121,6 @@ "eslint-plugin-promise": "^7.2.1", "eslint-plugin-security": "^1.7.1", "eslint-plugin-switch-statement": "^0.0.11", - "fast-check": "^4.6.0", "jest": "^29.3.1", "jest-junit": "^16.0.0", "jest-light-runner": "^0.4.1", @@ -3551,9 +3551,9 @@ } }, "node_modules/@roostorg/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@roostorg/types/-/types-2.0.0.tgz", - "integrity": "sha512-EyLecshJqiZdughsBgaJcR6nOmZ6cT5ioNeRB1Cif6b0ZrXc9LfBirXd2FPZ1qzxxIjgMUOA1HSxfFVThMRZlw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@roostorg/types/-/types-2.1.0.tgz", + "integrity": "sha512-nxPXjtAdf7ut/X/NYgbDl39r2wDKiO5vEeLPIHjV8MjDQ1/O5CbgJDyi1jvchMnIJW/AKskdask/Yu0J2JuICQ==", "license": "ISC", "dependencies": { "date-fns": "^2.29.3", @@ -14616,7 +14616,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", - "dev": true, "funding": [ { "type": "individual", @@ -14639,7 +14638,6 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "dev": true, "funding": [ { "type": "individual", diff --git a/server/package.json b/server/package.json index 9763529..aa5d118 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@opentelemetry/api": "^1.8.0", "@opentelemetry/semantic-conventions": "^1.22.0", "@roostorg/coop-integration-example": "^2.0.0", - "@roostorg/types": "^2.0.0", + "@roostorg/types": "^2.1.0", "@sendgrid/mail": "^8.1.6", "@stdlib/stats-binomial-test": "^0.0.7", "@total-typescript/ts-reset": "^0.3.7", diff --git a/server/services/itemProcessingService/fieldTypeHandlers.test.ts b/server/services/itemProcessingService/fieldTypeHandlers.test.ts index 36bcd60..ea33900 100644 --- a/server/services/itemProcessingService/fieldTypeHandlers.test.ts +++ b/server/services/itemProcessingService/fieldTypeHandlers.test.ts @@ -13,6 +13,42 @@ import { fieldTypeHandlers } from './fieldTypeHandlers.js'; describe('Content type schemas', () => { describe('fieldTypeHandlers', () => { + describe('IP_ADDRESS', () => { + const { coerce } = fieldTypeHandlers[ScalarTypes.IP_ADDRESS]; + + test('accepts valid IPv4 addresses', () => { + expect(coerce('192.0.2.1', [])).toBe('192.0.2.1'); + expect(coerce('203.0.113.255', [])).toBe('203.0.113.255'); + }); + + test('accepts valid IPv6 addresses', () => { + expect(coerce('2001:db8::1', [])).toBe('2001:db8::1'); + expect(coerce('::1', [])).toBe('::1'); + }); + + test('treats empty / whitespace-only strings as field omitted (returns null)', () => { + expect(coerce('', [])).toBeNull(); + expect(coerce(' ', [])).toBeNull(); + expect(coerce('\t\n', [])).toBeNull(); + }); + + test('strips leading/trailing whitespace before validating', () => { + expect(coerce(' 192.0.2.1', [])).toBe('192.0.2.1'); + expect(coerce('192.0.2.1 ', [])).toBe('192.0.2.1'); + expect(coerce(' 2600:1700:10f0:6d60:7007:c2c9:d164:bb47', [])).toBe( + '2600:1700:10f0:6d60:7007:c2c9:d164:bb47', + ); + }); + + test('rejects malformed IPs and non-string inputs', () => { + expect(coerce('not-an-ip', [])).toBeInstanceOf(Error); + expect(coerce('999.999.999.999', [])).toBeInstanceOf(Error); + expect(coerce('192.0.2.1 extra', [])).toBeInstanceOf(Error); + expect(coerce(42, [])).toBeInstanceOf(Error); + expect(coerce({ ip: '192.0.2.1' }, [])).toBeInstanceOf(Error); + }); + }); + test('should never accept null as a valid field value', () => { for (const [fieldType, handlers] of Object.entries(fieldTypeHandlers)) { if (!isContainerType(fieldType as keyof typeof fieldTypeHandlers)) { diff --git a/server/services/itemProcessingService/fieldTypeHandlers.ts b/server/services/itemProcessingService/fieldTypeHandlers.ts index c1f2460..3bca997 100644 --- a/server/services/itemProcessingService/fieldTypeHandlers.ts +++ b/server/services/itemProcessingService/fieldTypeHandlers.ts @@ -1,3 +1,4 @@ +import { isIP } from 'node:net'; import { ContainerTypes, makeDateString, @@ -80,10 +81,10 @@ type Handlers = { const scalarGetValues = (value: T): [T] => [value]; export const fieldTypeHandlers: Handlers = { -// NB: for ids (including user ids), we accept numbers or strings for user -// convenience, but we always coerce the value to a string so that we're not -// mixing strings and numbers in the same json column in the data warehouse (which -// could drastically reduce perf). + // NB: for ids (including user ids), we accept numbers or strings for user + // convenience, but we always coerce the value to a string so that we're not + // mixing strings and numbers in the same json column in the data warehouse (which + // could drastically reduce perf). [ScalarTypes.USER_ID]: { // TODO (COOP-745): USER_ID will be deprecated coerce: (v, legalItemTypeIds) => { @@ -130,8 +131,8 @@ export const fieldTypeHandlers: Handlers = { return urlString ? urlString : v === '' - ? null - : new Error('This field, if given, must be a valid URL.'); + ? null + : new Error('This field, if given, must be a valid URL.'); }, getValues: scalarGetValues, }, @@ -140,8 +141,8 @@ export const fieldTypeHandlers: Handlers = { typeof v === 'string' && !doesThrow(() => Geohash.decode(v)) ? v : v === '' - ? null - : new Error('This field, if given, must be a valid geohash.'), + ? null + : new Error('This field, if given, must be a valid geohash.'), getValues: scalarGetValues, }, [ScalarTypes.BOOLEAN]: { @@ -190,8 +191,10 @@ export const fieldTypeHandlers: Handlers = { return asDateString ? asDateString : v === '' - ? null - : new Error('This field, if given, must contain a valid date string.'); + ? null + : new Error( + 'This field, if given, must contain a valid date string.', + ); }, }, [ScalarTypes.RELATED_ITEM]: { @@ -218,6 +221,26 @@ export const fieldTypeHandlers: Handlers = { }, getValues: scalarGetValues, }, + [ScalarTypes.IP_ADDRESS]: { + // Accepts IPv4 and IPv6; rejects anything else. Leading/trailing + // whitespace is stripped. + // all-whitespace or empty string is treated as "field omitted". + coerce: (v) => { + if (typeof v !== 'string') { + return new Error('This field, if given, must be a string IP address.'); + } + const trimmed = v.trim(); + if (trimmed === '') { + return null; + } + return isIP(trimmed) > 0 + ? trimmed + : new Error( + 'This field, if given, must be a valid IPv4 or IPv6 address.', + ); + }, + getValues: scalarGetValues, + }, [ContainerTypes.ARRAY]: { coerce(value, itemTypeIds, container) { if (!Array.isArray(value)) { @@ -278,15 +301,15 @@ function coerceMediaUrlInput(value: unknown) { return typeof value !== 'string' ? err : value === '' - ? null - : isValidUrl(value) - ? // NB: `value` here CANNOT be typed as a UrlString, because we have some - // legacy submissions in the data warehouse where the string is not a valid URL. - // (Usually, it's the empty string, which previously got through.) - // TODO: replace all those submissions in the data warehouse with `field: null`, - // and then update the type here/in ScalarTypeRuntimeType. - { url: value } - : err; + ? null + : isValidUrl(value) + ? // NB: `value` here CANNOT be typed as a UrlString, because we have some + // legacy submissions in the data warehouse where the string is not a valid URL. + // (Usually, it's the empty string, which previously got through.) + // TODO: replace all those submissions in the data warehouse with `field: null`, + // and then update the type here/in ScalarTypeRuntimeType. + { url: value } + : err; } function coerceIdLikeInput(value: unknown) { @@ -296,8 +319,8 @@ function coerceIdLikeInput(value: unknown) { return typeof value === 'string' ? value : isFiniteNonNaNNumber(value) - ? String(value) - : new Error('This field must be a string or a number.'); + ? String(value) + : new Error('This field must be a string or a number.'); } function isFiniteNonNaNNumber(value: unknown) { diff --git a/server/services/moderationConfigService/dbTypes.ts b/server/services/moderationConfigService/dbTypes.ts index 13fc62b..bd9bbcf 100644 --- a/server/services/moderationConfigService/dbTypes.ts +++ b/server/services/moderationConfigService/dbTypes.ts @@ -32,6 +32,7 @@ export type ModerationConfigServicePg = { profile_icon_field: string | null; background_image_field: string | null; is_deleted_field: string | null; + ip_address_field: string | null; }; // TODO: redefine as a union to capture the correlation of the nulls, // then leverage FixKyselyRowCorrelation in the ItemTypesDbResult type. @@ -53,6 +54,7 @@ export type ModerationConfigServicePg = { profile_icon_field: GeneratedAlways; background_image_field: GeneratedAlways; is_deleted_field: GeneratedAlways; + ip_address_field: GeneratedAlways; version: GeneratedAlways; is_current: GeneratedAlways; }; @@ -98,7 +100,7 @@ export type ModerationConfigServicePg = { // whether to set `updated_at` on update or whether to just drop the column, // given the challenge of inerpreting the `updated_at` column on an entity // that has part of its data in other tables (e.g., should we update the - // action's updated_at when we update its set of item types?) + // action's updated_at when we update its set of item types?) created_at: GeneratedAlways; updated_at: Generated; applies_to_all_items_of_kind: Generated; diff --git a/server/services/moderationConfigService/moderationConfigService.test.ts b/server/services/moderationConfigService/moderationConfigService.test.ts index e410f32..212e3fa 100644 --- a/server/services/moderationConfigService/moderationConfigService.test.ts +++ b/server/services/moderationConfigService/moderationConfigService.test.ts @@ -304,6 +304,7 @@ describe('ModerationConfigService', () => { "createdAt": undefined, "creatorId": undefined, "displayName": "fakeField", + "ipAddress": undefined, "isDeleted": undefined, "parentId": undefined, "threadId": undefined, @@ -356,6 +357,7 @@ describe('ModerationConfigService', () => { "createdAt": undefined, "creatorId": undefined, "displayName": "fakeField", + "ipAddress": undefined, "isDeleted": undefined, }, "schemaVariant": "original", @@ -407,6 +409,7 @@ describe('ModerationConfigService', () => { "backgroundImage": undefined, "createdAt": undefined, "displayName": "fakeField", + "ipAddress": undefined, "isDeleted": undefined, "profileIcon": undefined, }, diff --git a/server/services/moderationConfigService/modules/ItemTypeOperations.ts b/server/services/moderationConfigService/modules/ItemTypeOperations.ts index 575d90d..8c7d67d 100644 --- a/server/services/moderationConfigService/modules/ItemTypeOperations.ts +++ b/server/services/moderationConfigService/modules/ItemTypeOperations.ts @@ -1,9 +1,9 @@ /* eslint-disable max-lines */ -import { type ConsumerDirectives } from '../../../lib/cache/index.js'; import { sql, type Kysely, type Selection } from 'kysely'; import { type ReadonlyDeep } from 'type-fest'; import { uid } from 'uid'; +import { type ConsumerDirectives } from '../../../lib/cache/index.js'; import { cached, type Cached } from '../../../utils/caching.js'; import { CoopError, @@ -50,6 +50,7 @@ const itemTypeDbSelection = [ 'is_deleted_field as isDeletedField', 'profile_icon_field as profileIconField', 'background_image_field as backgroundImageField', + 'ip_address_field as ipAddressField', 'org_id as orgId', 'is_default_user as isDefaultUserType', ] as const; @@ -262,6 +263,7 @@ export default class ItemTypeOperations { createdAt?: string | null; displayName?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ) { @@ -280,6 +282,7 @@ export default class ItemTypeOperations { created_at_field: input.schemaFieldRoles.createdAt, display_name_field: input.schemaFieldRoles.displayName, is_deleted_field: input.schemaFieldRoles.isDeleted, + ip_address_field: input.schemaFieldRoles.ipAddress, }) .returning('id') .executeTakeFirstOrThrow(); @@ -305,6 +308,7 @@ export default class ItemTypeOperations { createdAt?: string | null; displayName?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ) { @@ -333,6 +337,9 @@ export default class ItemTypeOperations { is_deleted_field: replaceEmptyStringWithNull( input.schemaFieldRoles.isDeleted, ), + ip_address_field: replaceEmptyStringWithNull( + input.schemaFieldRoles.ipAddress, + ), }), ) .where('id', '=', input.id) @@ -358,6 +365,7 @@ export default class ItemTypeOperations { displayName?: string | null; creatorId?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ): Promise { @@ -374,6 +382,7 @@ export default class ItemTypeOperations { display_name_field: input.schemaFieldRoles.displayName, creator_id_field: input.schemaFieldRoles.creatorId, is_deleted_field: input.schemaFieldRoles.isDeleted, + ip_address_field: input.schemaFieldRoles.ipAddress, }) .returning('id') .executeTakeFirstOrThrow(); @@ -397,6 +406,7 @@ export default class ItemTypeOperations { displayName?: string | null; creatorId?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ) { @@ -419,6 +429,9 @@ export default class ItemTypeOperations { is_deleted_field: replaceEmptyStringWithNull( input.schemaFieldRoles.isDeleted, ), + ip_address_field: replaceEmptyStringWithNull( + input.schemaFieldRoles.ipAddress, + ), }), ) .where('id', '=', input.id) @@ -445,6 +458,7 @@ export default class ItemTypeOperations { createdAt?: string | null; displayName?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ) { @@ -462,6 +476,7 @@ export default class ItemTypeOperations { created_at_field: input.schemaFieldRoles.createdAt, display_name_field: input.schemaFieldRoles.displayName, is_deleted_field: input.schemaFieldRoles.isDeleted, + ip_address_field: input.schemaFieldRoles.ipAddress, }) .returning('id') .executeTakeFirstOrThrow(); @@ -486,6 +501,7 @@ export default class ItemTypeOperations { createdAt?: string | null; displayName?: string | null; isDeleted?: string | null; + ipAddress?: string | null; }; }, ): Promise { @@ -511,6 +527,9 @@ export default class ItemTypeOperations { is_deleted_field: replaceEmptyStringWithNull( input.schemaFieldRoles.isDeleted, ), + ip_address_field: replaceEmptyStringWithNull( + input.schemaFieldRoles.ipAddress, + ), }), ) .where('id', '=', input.id) @@ -717,6 +736,7 @@ function dbResultToItemType( parentId: input.parentIdField ?? undefined, threadId: input.threadIdField ?? undefined, isDeleted: input.isDeletedField ?? undefined, + ipAddress: input.ipAddressField ?? undefined, // We collapse the cases here because otherwise this satisfies // check fails, but the correlation is checked by the DB // constraints @@ -727,6 +747,7 @@ function dbResultToItemType( createdAt: input.createdAtField ?? undefined, creatorId: input.creatorIdField ?? undefined, isDeleted: input.isDeletedField ?? undefined, + ipAddress: input.ipAddressField ?? undefined, } satisfies ThreadItemType['schemaFieldRoles']; case 'USER': return { @@ -735,6 +756,7 @@ function dbResultToItemType( createdAt: input.createdAtField ?? undefined, profileIcon: input.profileIconField ?? undefined, isDeleted: input.isDeletedField ?? undefined, + ipAddress: input.ipAddressField ?? undefined, } satisfies UserItemType['schemaFieldRoles']; default: assertUnreachable(input.kind); diff --git a/server/services/moderationConfigService/types/itemTypes.ts b/server/services/moderationConfigService/types/itemTypes.ts index 377be53..4de6c81 100644 --- a/server/services/moderationConfigService/types/itemTypes.ts +++ b/server/services/moderationConfigService/types/itemTypes.ts @@ -48,6 +48,7 @@ export type UserSchemaFieldRoles = { backgroundImage?: string; createdAt?: string; isDeleted?: string; + ipAddress?: string; }; export type ThreadSchemaFieldRoles = { @@ -55,6 +56,7 @@ export type ThreadSchemaFieldRoles = { createdAt?: string; creatorId?: string; isDeleted?: string; + ipAddress?: string; }; export type ContentSchemaFieldRoles = { @@ -62,6 +64,7 @@ export type ContentSchemaFieldRoles = { displayName?: string; creatorId?: string; isDeleted?: string; + ipAddress?: string; } & ( | { parentId?: string; @@ -119,6 +122,7 @@ export type FieldRoleToScalarType = { profileIcon: ScalarTypes['IMAGE']; backgroundImage: ScalarTypes['IMAGE']; isDeleted: ScalarTypes['BOOLEAN']; + ipAddress: ScalarTypes['IP_ADDRESS']; }; export function getPartialSchemaFromOriginal(schema: ItemSchema) { diff --git a/server/test/arbitraries/ContentType.ts b/server/test/arbitraries/ContentType.ts index 6fd97d1..d268c40 100644 --- a/server/test/arbitraries/ContentType.ts +++ b/server/test/arbitraries/ContentType.ts @@ -62,6 +62,19 @@ export const RelatedItemArbitrary = fc.record({ name: fc.string(), }); +// IETF reserved documentation ranges (RFC 5737 / RFC 3849) — safe to use in +// tests, will never collide with a real host. +export const IpAddressArbitrary = fc.oneof( + fc.ipV4(), + fc.constantFrom( + '203.0.113.1', + '198.51.100.42', + '192.0.2.255', + '2001:db8::1', + '2001:db8:1234:5678::1', + ), +); + export const ScalarValidValuesArbitraries = { [ScalarTypes.AUDIO]: MediaUrlArbitrary, [ScalarTypes.BOOLEAN]: fc.boolean(), @@ -79,6 +92,7 @@ export const ScalarValidValuesArbitraries = { [ScalarTypes.DATETIME]: DateStringArbitrary, [ScalarTypes.RELATED_ITEM]: RelatedItemArbitrary, [ScalarTypes.POLICY_ID]: IdLikeArbitrary, + [ScalarTypes.IP_ADDRESS]: IpAddressArbitrary, }; export const ScalarFieldArbitrary = fc @@ -124,18 +138,18 @@ export const MapFieldArbitrary = fc fc.string(), fc.boolean(), ) - .map>( - ([valueScalarType, keyScalarType, name, required]) => ({ - name, - required, - type: ContainerTypes.MAP, - container: { - containerType: ContainerTypes.MAP, - keyScalarType, - valueScalarType, - }, - }), - ); + .map< + Field + >(([valueScalarType, keyScalarType, name, required]) => ({ + name, + required, + type: ContainerTypes.MAP, + container: { + containerType: ContainerTypes.MAP, + keyScalarType, + valueScalarType, + }, + })); export const MapFieldWithValueArbitrary = MapFieldArbitrary.chain((field) => fc.tuple(