UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

345 lines (344 loc) 14.2 kB
import { eq, like } from "astro:db"; import { Effect, genLogger, pipe, Schema } from "../../../effect.js"; import { AstroDB } from "../effect/db.js"; import { noUndefinedEntries, parseData, parsedDataResponse, SelectPluginDataRespondOrFail } from "../effect/pluginUtils.js"; import { tsPluginData } from "../tables.js"; import { CacheContext, isCacheEnabled, isCacheExpired } from "../utils.js"; class SDKCore_PLUGINS extends Effect.Service()( "studiocms/sdk/SDKCore/modules/plugins", { dependencies: [AstroDB.Default], effect: genLogger("studiocms/sdk/SDKCore/modules/plugins/effect")(function* () { const [dbService, { pluginData }] = yield* Effect.all([AstroDB, CacheContext]); const _db = { /** * Creates a batch request function for querying plugin data from the database. * * The returned function accepts an object with `batchSize` and `offset` properties, * and performs a database query to select a batch of plugin data records with the specified * limit and offset. */ batchRequest: dbService.makeQuery( (query, o) => query((db) => db.select().from(tsPluginData).limit(o.batchSize).offset(o.offset)) ), /** * Retrieves plugin data entries from the database whose IDs match the specified plugin ID prefix. */ getEntriesPluginData: dbService.makeQuery( (query, pluginId) => query( (db) => db.select().from(tsPluginData).where(like(tsPluginData.id, `${pluginId}-%`)) ) ), /** * Executes a database query to select a single plugin data entry by its ID. */ selectPluginDataEntry: dbService.makeQuery( (query, id) => query((db) => db.select().from(tsPluginData).where(eq(tsPluginData.id, id)).get()) ), /** * Inserts a new plugin data entry into the database and returns the inserted record. */ insertPluginDataEntry: dbService.makeQuery( (query, data) => query((db) => db.insert(tsPluginData).values(data).returning().get()) ), /** * Updates an existing plugin data entry in the database. */ updatePluginDataEntry: dbService.makeQuery( (query, data) => query( (db) => db.update(tsPluginData).set(data).where(eq(tsPluginData.id, data.id)).returning().get() ) ) }; const initPluginDataCache = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/initPluginDataCache" )(function* (BATCH_SIZE) { let batchSize = BATCH_SIZE || 100; if (batchSize <= 0) { batchSize = 100; } let offset = 0; const sharedTimestamp = /* @__PURE__ */ new Date(); while (true) { const entries = yield* _db.batchRequest({ batchSize, offset }); if (entries.length === 0) break; const cacheEntries = entries.map( (entry) => [entry.id, { data: entry, lastCacheUpdate: sharedTimestamp }] ); for (const [id, cacheData] of cacheEntries) { pluginData.set(id, cacheData); } offset += batchSize; } }); const clearPluginDataCache = () => Effect.try({ try: () => pluginData.clear(), catch: () => new Error("Failed to clear plugin data cache") }); const _selectPluginDataEntry = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/_selectPluginDataEntry" )(function* (id) { if (yield* isCacheEnabled) { const cached = pluginData.get(id); if (cached && !isCacheExpired(cached)) { const { data: cacheData } = cached; return cacheData; } const fresh = yield* _db.selectPluginDataEntry(id); if (fresh) { pluginData.set(id, { data: fresh, lastCacheUpdate: /* @__PURE__ */ new Date() }); } return fresh; } return yield* _db.selectPluginDataEntry(id); }); const _insertPluginDataEntry = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/_insertPluginDataEntry" )(function* (data) { const newData = yield* _db.insertPluginDataEntry(data); if (yield* isCacheEnabled) { pluginData.set(newData.id, { data: newData, lastCacheUpdate: /* @__PURE__ */ new Date() }); } return newData; }); const _updatePluginDataEntry = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/_updatePluginDataEntry" )(function* (data) { const updatedData = yield* _db.updatePluginDataEntry(data); if (yield* isCacheEnabled) { pluginData.set(updatedData.id, { data: updatedData, lastCacheUpdate: /* @__PURE__ */ new Date() }); } return updatedData; }); const _processEntryFromCache = Effect.fn(function* ([key, { data: entry, lastCacheUpdate }], pluginId, validator) { if (!key.startsWith(pluginId)) { return void 0; } if ((yield* isCacheEnabled) && isCacheExpired({ lastCacheUpdate })) { const freshEntry = yield* _db.selectPluginDataEntry(entry.id); if (!freshEntry) { yield* Effect.log(`Removing stale cache entry: ${entry.id}`); pluginData.delete(entry.id); return void 0; } const validated2 = yield* parseData(freshEntry.data, validator); pluginData.set(entry.id, { data: freshEntry, lastCacheUpdate: /* @__PURE__ */ new Date() }); return yield* parsedDataResponse(entry.id, validated2); } const validated = yield* parseData(entry.data, validator); return yield* parsedDataResponse(entry.id, validated); }); const _processEntryFromDB = Effect.fn(function* (entry, validator) { const validated = yield* parseData(entry.data, validator); if (yield* isCacheEnabled) { pluginData.set(entry.id, { data: entry, lastCacheUpdate: /* @__PURE__ */ new Date() }); } return yield* parsedDataResponse(entry.id, validated); }); const _getEntries = Effect.fn("studiocms/sdk/SDKCore/modules/plugins/effect/_getEntries")( function* (pluginId, validator, filter) { if (yield* isCacheEnabled) { const data = yield* pipe( pluginData.entries(), Effect.forEach((entry) => _processEntryFromCache(entry, pluginId, validator)), Effect.map(noUndefinedEntries) ); if (data.length > 0) return data; } const todo = pipe( _db.getEntriesPluginData(pluginId), Effect.flatMap(Effect.forEach((entry) => _processEntryFromDB(entry, validator))) ); if (filter) return yield* todo.pipe(Effect.map(filter)); return yield* todo; } ); const _selectPluginDataEntryRespondOrFail = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/_selectPluginDataEntryRespondOrFail" )(function* (id, mode) { const existing = yield* _selectPluginDataEntry(id); switch (mode) { case SelectPluginDataRespondOrFail.ExistsNoFail: { if (existing) return existing; return void 0; } case SelectPluginDataRespondOrFail.ExistsShouldFail: { if (existing) return yield* Effect.fail(new Error(`Plugin data with ID ${id} already exists.`)); return void 0; } case SelectPluginDataRespondOrFail.NotExistsShouldFail: { if (!existing) return yield* Effect.fail(new Error(`Plugin data with ID ${id} does not exist.`)); return void 0; } default: return yield* Effect.fail(new Error(`Invalid mode: ${mode}`)); } }); const _select = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.select" )(function* (generatedEntryId, validator) { const existing = yield* _selectPluginDataEntryRespondOrFail( generatedEntryId, SelectPluginDataRespondOrFail.ExistsNoFail ); if (!existing) return void 0; const data = yield* parseData(existing.data, validator); return yield* parsedDataResponse(generatedEntryId, data); }); const _insert = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.insert" )(function* (generatedEntryId, data, validator) { yield* _selectPluginDataEntryRespondOrFail( generatedEntryId, SelectPluginDataRespondOrFail.ExistsShouldFail ); const parsedData = yield* parseData(data, validator); const inserted = yield* _insertPluginDataEntry({ id: generatedEntryId, data: parsedData }); return yield* parsedDataResponse(inserted.id, parsedData); }); const _update = Effect.fn( "studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.update" )(function* (generatedEntryId, data, validator) { yield* _selectPluginDataEntryRespondOrFail( generatedEntryId, SelectPluginDataRespondOrFail.NotExistsShouldFail ); const parsedData = yield* parseData(data, validator); const updated = yield* _updatePluginDataEntry({ id: generatedEntryId, data: parsedData }); return yield* parsedDataResponse(updated.id, parsedData); }); const buildReturn = (pluginId, entryId, validator) => { const generatedEntryId = `${pluginId}-${entryId}`; return { /** * Generates a unique ID for the plugin data entry. * * @returns An Effect that yields the generated ID. In the format `${pluginId}-${entryId}` */ generatedId: () => Effect.succeed(generatedEntryId), /** * Selects a plugin data entry by its ID, validating the data if a validator is provided. * * @returns An Effect that yields the selected plugin data entry or `undefined` if not found. */ select: () => _select(generatedEntryId, validator), /** * Inserts new plugin data into the database after validating the input. * * @param data - The plugin data to insert. * @yields Throws an error if validation fails or if the entry already exists. * @returns The parsed data response for the inserted entry. */ insert: (data) => _insert(generatedEntryId, data, validator), /** * Updates existing plugin data in the database after validating the input. * * @param data - The updated plugin data. * @yields Throws an error if validation fails. * @returns The parsed data response for the updated entry. */ update: (data) => _update(generatedEntryId, data, validator) }; }; function usePluginData(pluginId, { entryId, validator } = {}) { if (!entryId) { return { /** * Retrieves all plugin data entries for the specified plugin ID. * * @template T - The type of the plugin data object. * @param validator - Optional validator options for validating the plugin data. * @returns An Effect that yields an array of `PluginDataEntry<T>` objects. */ getEntries: (filter) => _getEntries(pluginId, validator, filter), getEntry: (id) => buildReturn(pluginId, id, validator) }; } return buildReturn(pluginId, entryId, validator); } class InferType { _Schema; $UsePluginData; $Insert; constructor(schema) { if (!schema || !Schema.isSchema(schema)) { throw new Error("InferType requires a valid Schema.Struct instance."); } this._Schema = schema; } } return { /** * Provides a set of effectful operations for managing plugin data entries by plugin ID and optional entry ID. * * When an `entryId` is provided, returns an object with methods to: * - Generate a unique plugin data entry ID. * - Insert new plugin data after validation and duplicate checks. * - Select and validate existing plugin data by ID. * - Update existing plugin data after validation. * * When no `entryId` is provided, returns an object with a method to retrieve all entries for the given plugin. * * @param pluginId - The unique identifier for the plugin. * @param entryId - (Optional) The unique identifier for the plugin data entry. * @returns An object with effectful methods for plugin data management, varying by presence of `entryId`. */ usePluginData, /** * Initializes the plugin data cache by fetching all existing entries from the database * and populating the in-memory cache with these entries. */ initPluginDataCache, /** * Clears the plugin data cache, removing all cached entries. * * @returns An Effect that resolves to `void` on success or an `Error` on failure. */ clearPluginDataCache, /** * Utility class to infer types from a given Schema. * * @typeParam S - The schema type extending `Schema.Struct<any>`. * * @property _Schema - The schema instance used for type inference. * @property usePluginData - The inferred type from the schema, used for plugin data. * @property Insert - A recursively simplified, mutable version of the schema's type. * */ InferType }; }) } ) { } export { SDKCore_PLUGINS };