Skip to content

Commit

Permalink
Implement deleting MediaExcerpts (#475)
Browse files Browse the repository at this point in the history
Also make URL/domain filters

---------

Signed-off-by: Carl Gieringer <[email protected]>
  • Loading branch information
carlgieringer authored Jul 18, 2023
1 parent 81eed28 commit dc400af
Show file tree
Hide file tree
Showing 20 changed files with 892 additions and 233 deletions.
8 changes: 8 additions & 0 deletions howdju-common/lib/apiModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ export const MediaExcerptSearchFilterKeys = [
"creatorUserId",
"speakerPersorgId",
"sourceId",
"domain",
/**
* Returns MediaExcerpts having URLs matching url.
*
* Matching means that the two are equal after removing the query parameters and fragment and
* ignoring the trailing slash. Both the `url` and `canonical_url` are considered.
*/
"url",
] as const;
export type MediaExcerptSearchFilter = ToFilter<
typeof MediaExcerptSearchFilterKeys
Expand Down
37 changes: 35 additions & 2 deletions howdju-common/lib/zodSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,11 @@ export const CreateUrlInput = CreateUrl;
export type CreateUrlInput = z.infer<typeof CreateUrlInput>;

export const UrlLocator = Entity.extend({
mediaExcerptId: z.string(),
url: Url,
anchors: z.array(DomAnchor).optional(),
created: momentObject,
creatorUserId: z.string(),
});
export type UrlLocator = z.output<typeof UrlLocator>;

Expand Down Expand Up @@ -449,7 +452,12 @@ export type SourceExcerptType = SourceExcerpt["type"];
/** @deprecated See SourceExcerpt */
export const SourceExcerptTypes = sourceExcerptTypes.Enum;

export const CreateUrlLocator = UrlLocator.omit({ id: true }).extend({
export const CreateUrlLocator = UrlLocator.omit({
id: true,
mediaExcerptId: true,
created: true,
creatorUserId: true,
}).extend({
url: CreateUrl,
anchors: z.array(CreateDomAnchor).optional(),
});
Expand Down Expand Up @@ -549,6 +557,23 @@ export type CreateMediaExcerptCitationInput = z.output<
typeof CreateMediaExcerptCitationInput
>;

/**
* A model identifying MediaExcerptCitations for deletion.
*
* Since MediaExcerptCitation is a relation and not an Entity (and so has no singular unique ID), we
* need a model to uniquely identify it for deletion.
*/
export const DeleteMediaExcerptCitation = z.object({
mediaExcerptId: z.string(),
source: z.object({
id: z.string(),
}),
normalPincite: z.string().optional(),
});
export type DeleteMediaExcerptCitation = z.output<
typeof DeleteMediaExcerptCitation
>;

/**
* A representation of an excerpt of some fixed media conveying speech. *
*
Expand Down Expand Up @@ -634,6 +659,9 @@ export const MediaExcerpt = Entity.extend({
citations: z.array(MediaExcerptCitation),
/** Persorgs to whom users have attributed the speech in the source excerpt. */
speakers: z.array(Persorg),
created: momentObject,
creatorUserId: z.string(),
creator: UserBlurb,
});
export type MediaExcerpt = z.output<typeof MediaExcerpt>;

Expand Down Expand Up @@ -1059,7 +1087,12 @@ export const CreateSourceExcerpt = z.discriminatedUnion("type", [
/** @deprecated */
export type CreateSourceExcerpt = z.infer<typeof CreateSourceExcerpt>;

const CreateMediaExcerptBase = MediaExcerpt.omit({ id: true })
const CreateMediaExcerptBase = MediaExcerpt.omit({
id: true,
created: true,
creatorUserId: true,
creator: true,
})
.merge(CreateModel)
.extend({
localRep: MediaExcerpt.shape.localRep.omit({ normalQuotation: true }),
Expand Down
113 changes: 101 additions & 12 deletions howdju-service-common/lib/daos/MediaExcerptsDao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { expectToBeSameMomentDeep, mockLogger } from "howdju-test-common";

import { endPoolAndDropDb, initDb, makeTestDbConfig } from "@/util/testUtil";
import { Database, makePool } from "../database";
import { MediaExcerptsDao } from "../daos";
import { MediaExcerptsDao, SourcesDao, UrlsDao } from "../daos";
import { makeTestProvider } from "@/initializers/TestProvider";
import TestHelper from "@/initializers/TestHelper";
import { MediaExcerptsService } from "..";
Expand All @@ -20,6 +20,8 @@ describe("MediaExcerptsDao", () => {

let dao: MediaExcerptsDao;
let mediaExcerptsService: MediaExcerptsService;
let sourcesDao: SourcesDao;
let urlsDao: UrlsDao;
let testHelper: TestHelper;
beforeEach(async () => {
dbName = await initDb(dbConfig);
Expand All @@ -31,14 +33,16 @@ describe("MediaExcerptsDao", () => {

dao = provider.mediaExcerptsDao;
mediaExcerptsService = provider.mediaExcerptsService;
sourcesDao = provider.sourcesDao;
urlsDao = provider.urlsDao;
testHelper = provider.testHelper;
});
afterEach(async () => {
await endPoolAndDropDb(pool, dbConfig, dbName);
});

describe("readMediaExcerptForId", () => {
test("reads a media excerpt for an ID", async () => {
test("reads a MediaExcerpt for an ID", async () => {
const { authToken, user } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });

Expand All @@ -53,6 +57,10 @@ describe("MediaExcerptsDao", () => {
expect(readMediaExcerpt).toEqual(
expectToBeSameMomentDeep(
merge({}, mediaExcerpt, {
creator: {
id: user.id,
longName: user.longName,
},
localRep: {
normalQuotation: "the text quote",
},
Expand Down Expand Up @@ -103,8 +111,73 @@ describe("MediaExcerptsDao", () => {

expect(readMediaExcerpt).toBeUndefined();
});
test.todo("doesn't read a deleted media excerpt");
test.todo("allows recreating a deleted media excerpt");
test("doesn't read a deleted MediaExcerpt", async () => {
const { authToken } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });
const deletedAt = utcNow();
await dao.deleteMediaExcerpt(mediaExcerpt.id, deletedAt);

const readMediaExcerpt = await dao.readMediaExcerptForId(mediaExcerpt.id);

expect(readMediaExcerpt).toBeUndefined();
});

test("reads a MediaExcerpt after deleting one of its Citations", async () => {
const { authToken } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });
const deletedAt = utcNow();
await dao.deleteMediaExcerptCitation(
mediaExcerpt.citations[0],
deletedAt
);

const readMediaExcerpt = await dao.readMediaExcerptForId(mediaExcerpt.id);

expect(readMediaExcerpt).toBeDefined();
});

test("reads a MediaExcerpt after deleting one of its Sources", async () => {
const { authToken } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });
const deletedAt = utcNow();
await sourcesDao.deleteSourceForId(
mediaExcerpt.citations[0].source.id,
deletedAt
);

// Act
const readMediaExcerpt = await dao.readMediaExcerptForId(mediaExcerpt.id);

expect(readMediaExcerpt).toBeDefined();
});
test("reads a MediaExcerpt after deleting one of its UrlLocators", async () => {
const { authToken } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });
const deletedAt = utcNow();
await dao.deleteUrlLocatorForId(
mediaExcerpt.locators.urlLocators[0].id,
deletedAt
);

// Act
const readMediaExcerpt = await dao.readMediaExcerptForId(mediaExcerpt.id);

expect(readMediaExcerpt).toBeDefined();
});
test("reads a MediaExcerpt after deleting one of its Urls", async () => {
const { authToken } = await testHelper.makeUser();
const mediaExcerpt = await testHelper.makeMediaExcerpt({ authToken });
const deletedAt = utcNow();
await urlsDao.deleteUrlForId(
mediaExcerpt.locators.urlLocators[0].url.id,
deletedAt
);

// Act
const readMediaExcerpt = await dao.readMediaExcerptForId(mediaExcerpt.id);

expect(readMediaExcerpt).toBeDefined();
});
});

describe("readEquivalentUrlLocator", () => {
Expand Down Expand Up @@ -300,6 +373,10 @@ describe("MediaExcerptsDao", () => {
citations: [expect.objectContaining(citations[0])],
created,
creatorUserId,
creator: {
id: creator.id,
longName: creator.longName,
},
})
);
});
Expand Down Expand Up @@ -400,8 +477,12 @@ describe("MediaExcerptsDao", () => {
);

// Act
const readMediaExcerpts = await dao.readMediaExcerptsMatchingUrl(
"https://www.example.com/the-path?otherKey=otherValue#other-fragment"
const readMediaExcerpts = await dao.readMediaExcerpts(
{
url: "https://www.example.com/the-path?otherKey=otherValue#other-fragment",
},
[],
5
);

// Assert
Expand Down Expand Up @@ -460,8 +541,12 @@ describe("MediaExcerptsDao", () => {
);

// Act
const readMediaExcerpts = await dao.readMediaExcerptsMatchingUrl(
"https://www.example.com/the-path?otherKey=otherValue#other-fragment"
const readMediaExcerpts = await dao.readMediaExcerpts(
{
url: "https://www.example.com/the-path?otherKey=otherValue#other-fragment",
},
[],
5
);

// Assert
Expand Down Expand Up @@ -518,8 +603,10 @@ describe("MediaExcerptsDao", () => {
);

// Act
const readMediaExcerpts = await dao.readMediaExcerptsMatchingDomain(
"www.example.com"
const readMediaExcerpts = await dao.readMediaExcerpts(
{ domain: "www.example.com" },
[],
5
);

// Assert
Expand Down Expand Up @@ -572,8 +659,10 @@ describe("MediaExcerptsDao", () => {
);

// Act
const readMediaExcerpts = await dao.readMediaExcerptsMatchingDomain(
"www.example.com"
const readMediaExcerpts = await dao.readMediaExcerpts(
{ domain: "www.example.com" },
[],
5
);

// Assert
Expand Down
Loading

0 comments on commit dc400af

Please sign in to comment.