
Security News
Package Maintainers Call for Improvements to GitHub’s New npm Security Plan
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
A really lightweight and typesafe package to group and merge related items from a flat array.
npm install -S flat-join
When using no frills databases (e.g. DynamoDB), simple data formats, etc, it's common not to be able to do SQL-like joins.
A common work around is to instead store related documents beside each other in a flat list, where the child documents have a key which is prefixed by the parent document's key.
For example, we might define a blog post and comments like so:
interface BlogPost {
id: string;
title: string;
description: string;
}
interface BlogPostComment {
id: string;
comment: string;
author: string;
}
Rather than storing these entities in separate collections, we could store them
in a flat list. We'd also need to add a discriminator field, that is, a field
that allows us to tell the different types apart. We'll call it type
.
const postsAndComments: (BlogPost | BlogPostComment)[] = [
{
type: "post",
id: "post-1",
title: "Test",
description: "Hello, world!",
},
{
type: "comment",
id: "post-1:comment-1",
comment: "Good job!",
author: "Dave",
},
{
type: "comment",
id: "post-1:comment-2",
comment: "Hmmm... :(",
author: "Bob",
},
{
type: "post",
id: "post-2",
title: "Test 2",
description: "Another test!",
},
];
As you can see, the comments related to the post with ID "post-1"
have an ID
prefixed with that value. Using a prefix is the most obvious way, since the
child documents must follow their parent document for the algorithm to be
efficient, and using a prefix ensures they're sorted in the correct order for
this.
Ideally, we'd want to hide this storage implementation detail from the rest of our app, and pass on a type that looks more like this:
interface BlogPostWithComments {
id: string;
title: string;
description: string;
comments: BlogPostComment[];
}
Enter flat-join
!
Using the types and data from above:
import { flatJoin } from "flat-join";
const postsWithComments = flatJoin(
// the data to join
postsAndComments,
// the value of `type` for the "primary" entity,
// i.e., the blog post itself
"post" as const,
// a mapping of the other entity types to the
// name of the field they are to be collected into
{ comment: "comments" } as const,
// additional options
{
// the name of the key that will be used as the ID
idKey: "id",
// the name of the key that will be used as the discriminator
typeKey: "type",
// a function specifying how to match children to parents
predicate: (childId: string, parentId: string) =>
childId.startsWith(parentId + ":"),
},
);
This will result in the following data structure:
const postsWithComments = [
{
type: "post",
id: "post-1",
title: "Test",
description: "Hello, world!",
comments: [
{
type: "comment",
id: "post-1:comment-1",
comment: "Good job!",
author: "Dave",
},
{
type: "comment",
id: "post-1:comment-2",
comment: "Hmmm... :(",
author: "Bob",
},
],
},
{
type: "post",
id: "post-2",
title: "Test 2",
description: "Another test!",
comments: [],
},
];
See how we're adding the ":"
to the startsWith
check? That's so that e.g.
post-11
comments don't end up on post-1
(since they have the same prefix).
The really cool part is that postsWithComments
will auto-magically have the
correct type!
IMPORTANT: for the inference to work you need to add as const
to the 2nd
and 3rd arguments.
Since in a given project, you'll probably use the same names for the ID and
discriminator keys, and the same predicate
function, for convenience you can
encapsulate the options:
const join = createJoinOn({
idKey: "id",
typeKey: "type",
predicate: (child: string, parent: string) => child.startsWith(parent + ":"),
});
const postsWithComments = join(
postsAndComments,
"post" as const,
{ comment: "comments" } as const,
);
There is an additional option not mentioned yet, throwOnOrphanedData
, which is
false
by default. If join
encounters data that it doesn't expect, it will
normally just silently ignore it. If you set throwOnOrphanedData
to true
, an
error will be thrown instead.
const join = createJoinOn({
idKey: "id",
typeKey: "type",
predicate: (child: string, parent: string) => child.startsWith(parent + ":"),
throwOnOrphanedData: true,
});
// throws OrphanedDataError
const postsWithComments = join(
[
{
type: "comment",
id: "post-1:comment-1",
comment: "Good job!",
author: "Dave",
},
{ type: "post", id: "post-1", title: "Test", description: "Hello, world!" },
{ type: "cat", id: "cat-1", name: "Socks" },
],
"post" as const,
{ comment: "comments" } as const,
);
Note that even though post-1
exists, since the join goes through the elements
in order, it won't have encountered it yet when it encounters the comment. Every
child of a given document must be directly after it with no 'primary' documents
or unrelated children in between. The first element must also be a primary
document.
If you know your data is not ordered like this, simply sort it before joining.
FAQs
Group and merge related items from a flat array.
We found that flat-join demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
Product
Socket Firewall is a free tool that blocks malicious packages at install time, giving developers proactive protection against rising supply chain attacks.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.