···77SECRETS_KEY= # optional, enables encrypted user secrets. openssl rand -base64 32
88NSID_ALLOWLIST=
99NSID_BLOCKLIST=
1010+# Hand-picked automation AT URIs, comma-separated. Shown on the homepage
1111+# and pinned at the top of the gallery tagged "Featured".
1212+# Example: at://did:plc:abc/run.airglow.automation/xyz,at://did:plc:def/run.airglow.automation/abc
1313+FEATURED_AUTOMATIONS=
···11+import { and, desc, eq, notInArray, or, sql } from "drizzle-orm";
22+import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
33+import { automations, users } from "../db/schema.js";
44+import type * as schema from "../db/schema.js";
55+66+// Both bun:sqlite and better-sqlite3 backends extend BaseSQLiteDatabase.
77+// Using the base lets tests use better-sqlite3 while prod uses bun:sqlite.
88+type Db = BaseSQLiteDatabase<"sync" | "async", unknown, typeof schema>;
99+1010+export type AutomationSearchResult = {
1111+ uri: string;
1212+ did: string;
1313+ rkey: string;
1414+ name: string;
1515+ description: string | null;
1616+ lexicon: string;
1717+ handle: string;
1818+};
1919+2020+export type AutomationSearchParams = {
2121+ q?: string;
2222+ lexicon?: string;
2323+ limit: number;
2424+ excludeUris?: string[];
2525+};
2626+2727+export async function searchAutomations(
2828+ db: Db,
2929+ params: AutomationSearchParams,
3030+): Promise<AutomationSearchResult[]> {
3131+ const conditions = [eq(automations.active, true)];
3232+3333+ if (params.lexicon) {
3434+ conditions.push(eq(automations.lexicon, params.lexicon));
3535+ }
3636+3737+ if (params.q) {
3838+ // Escape LIKE wildcards with a backslash and declare ESCAPE '\' so '%' and
3939+ // '_' in user input match literally instead of acting as wildcards.
4040+ const pattern = `%${params.q.replace(/[\\%_]/g, (m) => `\\${m}`)}%`;
4141+ const matcher = or(
4242+ sql`${automations.name} LIKE ${pattern} ESCAPE '\\'`,
4343+ sql`${automations.description} LIKE ${pattern} ESCAPE '\\'`,
4444+ sql`${automations.lexicon} LIKE ${pattern} ESCAPE '\\'`,
4545+ // actions is a JSON-mode TEXT column; LIKE matches targetCollection NSIDs too
4646+ sql`${automations.actions} LIKE ${pattern} ESCAPE '\\'`,
4747+ );
4848+ if (matcher) conditions.push(matcher);
4949+ }
5050+5151+ if (params.excludeUris && params.excludeUris.length > 0) {
5252+ conditions.push(notInArray(automations.uri, params.excludeUris));
5353+ }
5454+5555+ const whereClause = conditions.length === 1 ? conditions[0] : and(...conditions);
5656+5757+ return db
5858+ .select({
5959+ uri: automations.uri,
6060+ did: automations.did,
6161+ rkey: automations.rkey,
6262+ name: automations.name,
6363+ description: automations.description,
6464+ lexicon: automations.lexicon,
6565+ handle: users.handle,
6666+ })
6767+ .from(automations)
6868+ .innerJoin(users, eq(users.did, automations.did))
6969+ .where(whereClause)
7070+ .orderBy(desc(automations.indexedAt), desc(automations.uri))
7171+ .limit(params.limit);
7272+}
+6
lib/config.ts
···4444 secretsKey,
4545 nsidAllowlist: env("NSID_ALLOWLIST", "").split(",").filter(Boolean),
4646 nsidBlocklist: env("NSID_BLOCKLIST", "").split(",").filter(Boolean),
4747+ // Hand-picked AT URIs (at://did/run.airglow.automation/rkey), comma-separated.
4848+ // These appear on the homepage and at the top of the gallery tagged "Featured".
4949+ featuredAutomations: env("FEATURED_AUTOMATIONS", "")
5050+ .split(",")
5151+ .map((s) => s.trim())
5252+ .filter(Boolean),
4753} as const;